Merge pull request #585 from daviwil/mutators

Add mutators library for manipulating evaluated ADL types
This commit is contained in:
David Wilson 2021-06-21 09:49:47 -07:00 коммит произвёл GitHub
Родитель 34ea3a669a f2da3d1f5a
Коммит fec91682a8
8 изменённых файлов: 334 добавлений и 11 удалений

Просмотреть файл

@ -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");
});
});