Merge pull request #585 from daviwil/mutators
Add mutators library for manipulating evaluated ADL types
This commit is contained in:
Коммит
fec91682a8
|
@ -48,16 +48,23 @@ export interface TypeSymbol {
|
|||
}
|
||||
|
||||
export interface Binder {
|
||||
bindSourceFile(program: Program, sourceFile: ADLScriptNode): void;
|
||||
bindSourceFile(sourceFile: ADLScriptNode): void;
|
||||
bindNode(node: Node): void;
|
||||
}
|
||||
|
||||
export function createSymbolTable(): SymbolTable {
|
||||
return new SymbolTable();
|
||||
}
|
||||
|
||||
export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable) => void): Binder {
|
||||
export interface BinderOptions {
|
||||
// Configures the initial parent node to use when calling bindNode. This is
|
||||
// useful for binding ADL fragments outside the context of a full script node.
|
||||
initialParentNode?: Node;
|
||||
}
|
||||
|
||||
export function createBinder(program: Program, options: BinderOptions = {}): Binder {
|
||||
let currentFile: ADLScriptNode;
|
||||
let parentNode: Node;
|
||||
let parentNode: Node | undefined = options?.initialParentNode;
|
||||
let globalNamespace: NamespaceStatementNode;
|
||||
let fileNamespace: NamespaceStatementNode;
|
||||
let currentNamespace: NamespaceStatementNode;
|
||||
|
@ -66,9 +73,10 @@ export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable)
|
|||
let scope: ScopeNode;
|
||||
return {
|
||||
bindSourceFile,
|
||||
bindNode,
|
||||
};
|
||||
|
||||
function bindSourceFile(program: Program, sourceFile: ADLScriptNode) {
|
||||
function bindSourceFile(sourceFile: ADLScriptNode) {
|
||||
globalNamespace = program.globalNamespace;
|
||||
fileNamespace = globalNamespace;
|
||||
currentFile = sourceFile;
|
||||
|
@ -121,7 +129,7 @@ export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable)
|
|||
visitChildren(node, bindNode);
|
||||
|
||||
if (node.kind !== SyntaxKind.NamespaceStatement) {
|
||||
reportDuplicateSymbols(node.locals!);
|
||||
program.reportDuplicateSymbols(node.locals!);
|
||||
}
|
||||
|
||||
scope = prevScope;
|
||||
|
|
|
@ -49,6 +49,8 @@ import {
|
|||
export interface Checker {
|
||||
getTypeForNode(node: Node): Type;
|
||||
checkProgram(program: Program): void;
|
||||
checkModelProperty(prop: ModelPropertyNode): ModelTypeProperty;
|
||||
checkUnionExpression(node: UnionExpressionNode): UnionType;
|
||||
getGlobalNamespaceType(): NamespaceType;
|
||||
|
||||
getLiteralType(node: StringLiteralNode): StringLiteralType;
|
||||
|
@ -153,6 +155,8 @@ export function createChecker(program: Program): Checker {
|
|||
return {
|
||||
getTypeForNode,
|
||||
checkProgram,
|
||||
checkModelProperty,
|
||||
checkUnionExpression,
|
||||
getLiteralType,
|
||||
getTypeName,
|
||||
getNamespaceString,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export * from "../lib/decorators.js";
|
||||
export * from "./diagnostics.js";
|
||||
export * from "./mutators.js";
|
||||
export * from "./parser.js";
|
||||
export * from "./program.js";
|
||||
export * from "./types.js";
|
||||
|
||||
import * as formatter from "../formatter/index.js";
|
||||
export const ADLPrettierPlugin = formatter;
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
import { createBinder } from "./binder.js";
|
||||
import { parse } from "./parser.js";
|
||||
import { Program } from "./program.js";
|
||||
import {
|
||||
ModelExpressionNode,
|
||||
ModelPropertyNode,
|
||||
ModelStatementNode,
|
||||
ModelType,
|
||||
ModelTypeProperty,
|
||||
Node,
|
||||
OperationStatementNode,
|
||||
OperationType,
|
||||
SyntaxKind,
|
||||
UnionExpressionNode,
|
||||
} from "./types.js";
|
||||
|
||||
function addProperty(
|
||||
program: Program,
|
||||
model: ModelType,
|
||||
modelNode: ModelStatementNode | ModelExpressionNode,
|
||||
parentNode: Node,
|
||||
propertyName: string,
|
||||
propertyTypeName: string,
|
||||
insertIndex?: number
|
||||
): ModelTypeProperty | undefined {
|
||||
// Parse a temporary model type to extract its property
|
||||
const fakeNode = parse(`model Fake { ${propertyName}: ${propertyTypeName}}`);
|
||||
if (fakeNode.parseDiagnostics.length > 0) {
|
||||
program.reportDiagnostic(
|
||||
`Could not add property/parameter "${propertyName}" of type "${propertyTypeName}"`,
|
||||
model
|
||||
);
|
||||
program.reportDiagnostics(fakeNode.parseDiagnostics);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const firstStatement = fakeNode.statements[0] as ModelStatementNode;
|
||||
const graftProperty = firstStatement.properties![0] as ModelPropertyNode;
|
||||
|
||||
// Fix up the source location of the nodes to match the model node that
|
||||
// contains the new property since we can't update the entire file's node
|
||||
// positions.
|
||||
graftProperty.pos = modelNode.pos;
|
||||
graftProperty.end = modelNode.end;
|
||||
|
||||
// Create a binder to wire up the grafted property
|
||||
const binder = createBinder(program, {
|
||||
initialParentNode: parentNode,
|
||||
});
|
||||
binder.bindNode(graftProperty);
|
||||
|
||||
// Evaluate the new property with the checker
|
||||
const newProperty = program.checker!.checkModelProperty(graftProperty);
|
||||
|
||||
// Put the property back into the node
|
||||
modelNode.properties.splice(insertIndex || modelNode.properties.length, 0, graftProperty);
|
||||
if (insertIndex !== undefined) {
|
||||
// Insert the property by adding it in the right order to a new Map
|
||||
let i = 0;
|
||||
const newProperties = new Map<string, ModelTypeProperty>();
|
||||
for (let [name, prop] of model.properties.entries()) {
|
||||
if (i === insertIndex) {
|
||||
newProperties.set(newProperty.name, newProperty);
|
||||
}
|
||||
newProperties.set(name, prop);
|
||||
model.properties = newProperties;
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
model.properties.set(newProperty.name, newProperty);
|
||||
}
|
||||
|
||||
return newProperty;
|
||||
}
|
||||
|
||||
export function addModelProperty(
|
||||
program: Program,
|
||||
model: ModelType,
|
||||
propertyName: string,
|
||||
propertyTypeName: string
|
||||
): ModelTypeProperty | undefined {
|
||||
if (model.node.kind !== SyntaxKind.ModelStatement) {
|
||||
program.reportDiagnostic(
|
||||
"Cannot add a model property to anything except a model statement.",
|
||||
model
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the property and add it to the type
|
||||
const newProperty = addProperty(
|
||||
program,
|
||||
model,
|
||||
model.node,
|
||||
model.node,
|
||||
propertyName,
|
||||
propertyTypeName
|
||||
);
|
||||
|
||||
if (newProperty) {
|
||||
model.properties.set(propertyName, newProperty);
|
||||
return newProperty;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface NewParameterOptions {
|
||||
// Insert the parameter at the specified index. If `undefined`, add the
|
||||
// parameter to the end of the parameter list.
|
||||
insertIndex?: number;
|
||||
}
|
||||
|
||||
export function addOperationParameter(
|
||||
program: Program,
|
||||
operation: OperationType,
|
||||
parameterName: string,
|
||||
parameterTypeName: string,
|
||||
options?: NewParameterOptions
|
||||
): ModelTypeProperty | undefined {
|
||||
if (operation.node.kind !== SyntaxKind.OperationStatement) {
|
||||
program.reportDiagnostic(
|
||||
"Cannot add a parameter to anything except an operation statement.",
|
||||
operation
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the property and add it to the type
|
||||
return addProperty(
|
||||
program,
|
||||
operation.parameters,
|
||||
operation.node.parameters,
|
||||
operation.node,
|
||||
parameterName,
|
||||
parameterTypeName,
|
||||
options?.insertIndex
|
||||
);
|
||||
}
|
||||
|
||||
export function addOperationResponseType(
|
||||
program: Program,
|
||||
operation: OperationType,
|
||||
responseTypeName: string
|
||||
): any {
|
||||
if (operation.node.kind !== SyntaxKind.OperationStatement) {
|
||||
program.reportDiagnostic(
|
||||
"Cannot add a response to anything except an operation statement.",
|
||||
operation
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse a temporary operation to extract its response type
|
||||
const opNode = parse(`op Fake(): string | ${responseTypeName};`);
|
||||
if (opNode.parseDiagnostics.length > 0) {
|
||||
program.reportDiagnostic(
|
||||
`Could not add response type "${responseTypeName}" to operation ${operation.name}"`,
|
||||
operation
|
||||
);
|
||||
program.reportDiagnostics(opNode.parseDiagnostics);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const graftUnion = (opNode.statements[0] as OperationStatementNode)
|
||||
.returnType as UnionExpressionNode;
|
||||
|
||||
// Graft the union into the operation
|
||||
const originalResponse = operation.node.returnType;
|
||||
graftUnion.options[0] = originalResponse;
|
||||
operation.node.returnType = graftUnion;
|
||||
|
||||
// Create a binder to wire up the grafted property
|
||||
const binder = createBinder(program, {
|
||||
initialParentNode: operation.node,
|
||||
});
|
||||
binder.bindNode(graftUnion);
|
||||
|
||||
// Evaluate the new response type with the checker
|
||||
operation.returnType = program.checker!.checkUnionExpression(graftUnion);
|
||||
}
|
|
@ -90,7 +90,7 @@ export async function createProgram(
|
|||
};
|
||||
|
||||
let virtualFileCount = 0;
|
||||
const binder = createBinder(program.reportDuplicateSymbols);
|
||||
const binder = createBinder(program);
|
||||
|
||||
if (!options?.nostdlib) {
|
||||
await loadStandardLibrary(program);
|
||||
|
@ -252,7 +252,7 @@ export async function createProgram(
|
|||
|
||||
program.reportDiagnostics(sourceFile.parseDiagnostics);
|
||||
program.sourceFiles.push(sourceFile);
|
||||
binder.bindSourceFile(program, sourceFile);
|
||||
binder.bindSourceFile(sourceFile);
|
||||
await evalImports(sourceFile);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ export interface OperationType {
|
|||
node: OperationStatementNode;
|
||||
name: string;
|
||||
namespace?: NamespaceType;
|
||||
parameters?: ModelType;
|
||||
parameters: ModelType;
|
||||
returnType: Type;
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,7 @@ export interface OperationStatementNode extends BaseNode, DeclarationNode {
|
|||
export interface ModelStatementNode extends BaseNode, DeclarationNode {
|
||||
kind: SyntaxKind.ModelStatement;
|
||||
id: IdentifierNode;
|
||||
properties?: (ModelPropertyNode | ModelSpreadPropertyNode)[];
|
||||
properties: (ModelPropertyNode | ModelSpreadPropertyNode)[];
|
||||
heritage: ReferenceExpression[];
|
||||
templateParameters: TemplateParameterDeclarationNode[];
|
||||
locals?: SymbolTable;
|
||||
|
|
|
@ -154,7 +154,12 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
addJsFile("/.adl/test-lib/test.js", {
|
||||
test(_: any, target: Type, name?: string) {
|
||||
if (!name) {
|
||||
if (target.kind === "Model" || target.kind === "Namespace" || target.kind === "Enum") {
|
||||
if (
|
||||
target.kind === "Model" ||
|
||||
target.kind === "Namespace" ||
|
||||
target.kind === "Enum" ||
|
||||
target.kind === "Operation"
|
||||
) {
|
||||
name = target.name;
|
||||
} else {
|
||||
throw new Error("Need to specify a name for test type");
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import { strictEqual } from "assert";
|
||||
import {
|
||||
addModelProperty,
|
||||
addOperationParameter,
|
||||
addOperationResponseType,
|
||||
} from "../compiler/mutators.js";
|
||||
import { Program } from "../compiler/program.js";
|
||||
import { ModelType, OperationType, UnionType } from "../compiler/types.js";
|
||||
import { createTestHost, TestHost } from "./test-host.js";
|
||||
|
||||
describe("adl: mutators", () => {
|
||||
let testHost: TestHost;
|
||||
|
||||
beforeEach(async () => {
|
||||
testHost = await createTestHost();
|
||||
});
|
||||
|
||||
function addBarProperty(program: Program, model: ModelType) {
|
||||
addModelProperty(program, model, "bar", "string");
|
||||
}
|
||||
|
||||
it("addModelProperty adds a property to a model type", async () => {
|
||||
testHost.addJsFile("a.js", { addBarProperty });
|
||||
testHost.addAdlFile(
|
||||
"main.adl",
|
||||
`
|
||||
import "./a.js";
|
||||
|
||||
@test
|
||||
@addBarProperty
|
||||
model A { foo: int32; }
|
||||
`
|
||||
);
|
||||
|
||||
const { A } = (await testHost.compile("./")) as { A: ModelType };
|
||||
|
||||
strictEqual(A.properties.size, 2);
|
||||
strictEqual(A.properties.get("bar")!.name, "bar");
|
||||
strictEqual((A.properties.get("bar")!.type as ModelType).name, "string");
|
||||
});
|
||||
|
||||
function addParameters(program: Program, operation: OperationType) {
|
||||
addOperationParameter(program, operation, "omega", "string");
|
||||
addOperationParameter(program, operation, "alpha", "int64", { insertIndex: 0 });
|
||||
addOperationParameter(program, operation, "beta", "B.Excellent", { insertIndex: 1 });
|
||||
}
|
||||
|
||||
it("addOperationParameter inserts operation parameters", async () => {
|
||||
testHost.addJsFile("a.js", { addParameters });
|
||||
testHost.addAdlFile(
|
||||
"main.adl",
|
||||
`
|
||||
import "./a.adl";
|
||||
import "./b.adl";
|
||||
`
|
||||
);
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
import "./a.js";
|
||||
import "./b.adl";
|
||||
|
||||
@test
|
||||
@addParameters
|
||||
op TestOp(foo: int32): string;
|
||||
`
|
||||
);
|
||||
testHost.addAdlFile(
|
||||
"b.adl",
|
||||
`
|
||||
namespace B;
|
||||
model Excellent {}
|
||||
`
|
||||
);
|
||||
|
||||
const { TestOp } = (await testHost.compile("./")) as { TestOp: OperationType };
|
||||
|
||||
const params = Array.from(TestOp.parameters.properties.entries());
|
||||
strictEqual(TestOp.parameters.properties.size, 4);
|
||||
strictEqual(params[0][0], "alpha");
|
||||
strictEqual((params[0][1].type as ModelType).name, "int64");
|
||||
strictEqual(params[1][0], "beta");
|
||||
strictEqual((params[1][1].type as ModelType).name, "Excellent");
|
||||
strictEqual(params[2][0], "foo");
|
||||
strictEqual((params[2][1].type as ModelType).name, "int32");
|
||||
strictEqual(params[3][0], "omega");
|
||||
strictEqual((params[3][1].type as ModelType).name, "string");
|
||||
});
|
||||
|
||||
function addResponseTypes(program: Program, operation: OperationType) {
|
||||
addOperationResponseType(program, operation, "int64");
|
||||
addOperationResponseType(program, operation, "A.Response");
|
||||
}
|
||||
|
||||
it("addModelProperty adds a property to a model type", async () => {
|
||||
testHost.addJsFile("a.js", { addResponseTypes });
|
||||
testHost.addAdlFile(
|
||||
"main.adl",
|
||||
`
|
||||
import "./a.js";
|
||||
|
||||
@test
|
||||
@addResponseTypes
|
||||
op TestOp(foo: int32): string;
|
||||
|
||||
namespace A {
|
||||
model Response {}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { TestOp } = (await testHost.compile("./")) as { TestOp: OperationType };
|
||||
|
||||
const unionType = TestOp.returnType as UnionType;
|
||||
strictEqual(unionType.options.length, 3);
|
||||
strictEqual((unionType.options[0] as ModelType).name, "string");
|
||||
strictEqual((unionType.options[1] as ModelType).name, "int64");
|
||||
strictEqual((unionType.options[2] as ModelType).name, "Response");
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче