Refactor projections test to make sure we convert (#1509)

This commit is contained in:
Timothee Guerin 2023-01-05 14:53:41 -08:00 коммит произвёл GitHub
Родитель 5ee584b3cb
Коммит 3c208033fd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 415 добавлений и 248 удалений

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

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/compiler",
"comment": "Fix issue with referencing spread properties or enum member depending on the order of declaration",
"type": "none"
}
],
"packageName": "@cadl-lang/compiler"
}

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

@ -2472,100 +2472,108 @@ export function createChecker(program: Program): Checker {
}
function bindAllMembers(node: Node) {
const bound = new Set<Sym>();
if (node.symbol) {
bindMembers(node, node.symbol);
}
visitChildren(node, (child) => {
bindAllMembers(child);
});
}
function bindMembers(node: Node, containerSym: Sym) {
let containerMembers: Mutable<SymbolTable>;
switch (node.kind) {
case SyntaxKind.ModelStatement:
if (node.extends && node.extends.kind === SyntaxKind.TypeReference) {
resolveAndCopyMembers(node.extends);
}
if (node.is && node.is.kind === SyntaxKind.TypeReference) {
resolveAndCopyMembers(node.is);
}
for (const prop of node.properties) {
if (prop.kind === SyntaxKind.ModelSpreadProperty) {
resolveAndCopyMembers(prop.target);
} else {
const name = prop.id.kind === SyntaxKind.Identifier ? prop.id.sv : prop.id.value;
bindMember(name, prop, SymbolFlags.ModelProperty);
}
}
break;
case SyntaxKind.EnumStatement:
for (const member of node.members.values()) {
if (member.kind === SyntaxKind.EnumSpreadMember) {
resolveAndCopyMembers(member.target);
} else {
const name = member.id.kind === SyntaxKind.Identifier ? member.id.sv : member.id.value;
bindMember(name, member, SymbolFlags.EnumMember);
}
}
break;
case SyntaxKind.InterfaceStatement:
for (const member of node.operations.values()) {
bindMember(member.id.sv, member, SymbolFlags.InterfaceMember | SymbolFlags.Operation);
}
if (node.extends) {
for (const ext of node.extends) {
resolveAndCopyMembers(ext);
}
}
break;
case SyntaxKind.UnionStatement:
for (const variant of node.options.values()) {
const name = variant.id.kind === SyntaxKind.Identifier ? variant.id.sv : variant.id.value;
bindMember(name, variant, SymbolFlags.UnionVariant);
}
break;
}
function resolveAndCopyMembers(node: TypeReferenceNode) {
let ref = resolveTypeReferenceSym(node, undefined);
if (ref && ref.flags & SymbolFlags.Alias) {
ref = resolveAliasedSymbol(ref);
function bindMembers(node: Node, containerSym: Sym) {
if (bound.has(containerSym)) {
return;
}
if (ref && ref.members) {
copyMembers(ref.members);
}
}
bound.add(containerSym);
let containerMembers: Mutable<SymbolTable>;
function resolveAliasedSymbol(ref: Sym): Sym | undefined {
const node = ref.declarations[0] as AliasStatementNode;
switch (node.value.kind) {
case SyntaxKind.MemberExpression:
case SyntaxKind.TypeReference:
case SyntaxKind.Identifier:
const resolvedSym = resolveTypeReferenceSym(node.value, undefined);
if (resolvedSym && resolvedSym.flags & SymbolFlags.Alias) {
return resolveAliasedSymbol(resolvedSym);
switch (node.kind) {
case SyntaxKind.ModelStatement:
if (node.extends && node.extends.kind === SyntaxKind.TypeReference) {
resolveAndCopyMembers(node.extends);
}
return resolvedSym;
default:
return undefined;
if (node.is && node.is.kind === SyntaxKind.TypeReference) {
resolveAndCopyMembers(node.is);
}
for (const prop of node.properties) {
if (prop.kind === SyntaxKind.ModelSpreadProperty) {
resolveAndCopyMembers(prop.target);
} else {
const name = prop.id.kind === SyntaxKind.Identifier ? prop.id.sv : prop.id.value;
bindMember(name, prop, SymbolFlags.ModelProperty);
}
}
break;
case SyntaxKind.EnumStatement:
for (const member of node.members.values()) {
if (member.kind === SyntaxKind.EnumSpreadMember) {
resolveAndCopyMembers(member.target);
} else {
const name =
member.id.kind === SyntaxKind.Identifier ? member.id.sv : member.id.value;
bindMember(name, member, SymbolFlags.EnumMember);
}
}
break;
case SyntaxKind.InterfaceStatement:
for (const member of node.operations.values()) {
bindMember(member.id.sv, member, SymbolFlags.InterfaceMember | SymbolFlags.Operation);
}
if (node.extends) {
for (const ext of node.extends) {
resolveAndCopyMembers(ext);
}
}
break;
case SyntaxKind.UnionStatement:
for (const variant of node.options.values()) {
const name =
variant.id.kind === SyntaxKind.Identifier ? variant.id.sv : variant.id.value;
bindMember(name, variant, SymbolFlags.UnionVariant);
}
break;
}
}
function copyMembers(table: SymbolTable) {
const members = augmentedSymbolTables.get(table) ?? table;
for (const member of members.values()) {
bindMember(member.name, member.declarations[0], member.flags);
function resolveAndCopyMembers(node: TypeReferenceNode) {
let ref = resolveTypeReferenceSym(node, undefined);
if (ref && ref.flags & SymbolFlags.Alias) {
ref = resolveAliasedSymbol(ref);
}
if (ref && ref.members) {
bindMembers(ref.declarations[0], ref);
copyMembers(ref.members);
}
}
}
function bindMember(name: string, node: Node, kind: SymbolFlags) {
const sym = createSymbol(node, name, kind, containerSym);
compilerAssert(containerSym.members, "containerSym.members is undefined");
containerMembers ??= getOrCreateAugmentedSymbolTable(containerSym.members);
containerMembers.set(name, sym);
function resolveAliasedSymbol(ref: Sym): Sym | undefined {
const node = ref.declarations[0] as AliasStatementNode;
switch (node.value.kind) {
case SyntaxKind.MemberExpression:
case SyntaxKind.TypeReference:
case SyntaxKind.Identifier:
const resolvedSym = resolveTypeReferenceSym(node.value, undefined);
if (resolvedSym && resolvedSym.flags & SymbolFlags.Alias) {
return resolveAliasedSymbol(resolvedSym);
}
return resolvedSym;
default:
return undefined;
}
}
function copyMembers(table: SymbolTable) {
const members = augmentedSymbolTables.get(table) ?? table;
for (const member of members.values()) {
bindMember(member.name, member.declarations[0], member.flags);
}
}
function bindMember(name: string, node: Node, kind: SymbolFlags) {
const sym = createSymbol(node, name, kind, containerSym);
compilerAssert(containerSym.members, "containerSym.members is undefined");
containerMembers ??= getOrCreateAugmentedSymbolTable(containerSym.members);
containerMembers.set(name, sym);
}
}
}

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

@ -389,13 +389,14 @@ export function createProjector(
returnType,
});
if (op.interface) {
projectedOp.interface = projectedInterfaceScope();
} else if (op.namespace) {
if (op.namespace) {
projectedOp.namespace = projectedNamespaceScope();
}
finishTypeForProgram(projectedProgram, projectedOp);
if (op.interface) {
projectedOp.interface = projectType(op.interface) as Interface;
}
return applyProjection(op, projectedOp);
}
@ -453,9 +454,8 @@ export function createProjector(
decorators: projectedDecs,
});
const parentUnion = projectType(variant.union) as Union;
projectedVariant.union = parentUnion;
finishTypeForProgram(projectedProgram, projectedVariant);
projectedVariant.union = projectType(variant.union) as Union;
return projectedVariant;
}
@ -498,9 +498,8 @@ export function createProjector(
const projectedMember = shallowClone(e, {
decorators,
});
const parentEnum = projectType(e.enum) as Enum;
projectedMember.enum = parentEnum;
finishTypeForProgram(projectedProgram, projectedMember);
projectedMember.enum = projectType(e.enum) as Enum;
return projectedMember;
}
@ -563,25 +562,6 @@ export function createProjector(
return projectType(ns) as Namespace;
}
function interfaceScope(): Interface | undefined {
for (let i = scope.length - 1; i >= 0; i--) {
if ("interface" in scope[i]) {
return (scope[i] as any).interface;
}
}
return undefined;
}
function projectedInterfaceScope(): Interface | undefined {
const iface = interfaceScope();
if (!iface) return iface;
if (!projectedTypes.has(iface)) {
throw new Error(`Interface "${iface.name}" should have been projected already`);
}
return projectType(iface) as Interface;
}
function applyProjection(baseType: Type, projectedType: Type): Type {
const inScopeProjections = getInScopeProjections();
for (const projectionApplication of inScopeProjections) {
@ -607,9 +587,6 @@ export function createProjector(
if ("namespace" in type && type.namespace !== undefined) {
scopeProps.namespace = projectedNamespaceScope();
}
if ("interface" in type && type.interface !== undefined) {
scopeProps.interface = projectedInterfaceScope();
}
const clone = checker.createType({
...type,

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

@ -44,7 +44,7 @@ describe("compiler: references", () => {
ref: "MyModel.x",
}));
describe("spread property", () =>
describe("spread property from model defined before", () =>
itCanReference({
code: `
model Spreadable {
@ -59,6 +59,21 @@ describe("compiler: references", () => {
resolveTarget: (target: Model) => target.properties.get("y"),
}));
describe("spread property from model defined after", () =>
itCanReference({
code: `
@test("target") model MyModel {
x: string;
... Spreadable;
}
model Spreadable {
y: string;
}`,
ref: "MyModel.y",
resolveTarget: (target: Model) => target.properties.get("y"),
}));
describe("spread property via alias", () =>
itCanReference({
code: `

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

@ -8,7 +8,6 @@ import {
EnumMember,
Interface,
Model,
ModelProperty,
Namespace,
NumericLiteral,
Operation,
@ -20,7 +19,7 @@ import {
import { getDoc } from "../../lib/decorators.js";
import { createTestHost, TestHost } from "../../testing/index.js";
describe("compiler: projections", () => {
describe("compiler: projections: logic", () => {
let testHost: TestHost;
beforeEach(async () => {
@ -213,70 +212,6 @@ describe("compiler: projections", () => {
});
describe("models", () => {
it("link projected model to projected properties", async () => {
const code = `
@test model Foo {
name: string;
}
#suppress "projections-are-experimental"
projection model#test {to {}}`;
const result = (await testProjection(code)) as Model;
ok(result.projectionBase);
strictEqual(result.properties.get("name")?.model, result);
});
it("link projected property with sourceProperty", async () => {
const code = `
@test model Foo {
...Bar
}
model Bar {
name: string;
}
#suppress "projections-are-experimental"
projection Foo#test {to {}}`;
const Foo = (await testProjection(code)) as Model;
ok(Foo.projectionBase);
const sourceProperty = Foo.properties.get("name")?.sourceProperty;
ok(sourceProperty);
strictEqual(sourceProperty, Foo.namespace!.models.get("Bar")?.properties.get("name"));
strictEqual(sourceProperty.model, Foo.namespace!.models.get("Bar"));
});
it("project all properties first", async () => {
const keySym = Symbol("key");
testHost.addJsFile("lib.js", {
$tagProp({ program }: DecoratorContext, t: ModelProperty) {
program.stateSet(keySym).add(t);
},
$tagModel({ program }: DecoratorContext, t: Model) {
for (const prop of t.properties.values()) {
ok(
program.stateSet(keySym).has(prop),
`Prop ${prop.name} should have run @key decorator by this time.`
);
}
},
});
const code = `
import "./lib.js";
@test @tagModel model Foo {
...Bar;
}
model Bar {
@tagProp name: string;
}
#suppress "projections-are-experimental"
projection Foo#test {to {}}`;
await testProjection(code);
});
it("works for versioning", async () => {
const addedOnKey = Symbol("addedOn");
const removedOnKey = Symbol("removedOn");
@ -362,46 +297,6 @@ describe("compiler: projections", () => {
strictEqual(resultNested2.properties.size, 2);
});
it("runs decorator on property before model", async () => {
const collection: Type[] = [];
testHost.addJsFile("./ref.js", {
$ref: (_: DecoratorContext, target: Type) => collection.push(target),
});
const code = `
import "./ref.js";
@test model Bar {
b: Foo.b;
}
@ref
@test model Foo {
@ref
b: string;
}
#suppress "projections-are-experimental"
projection model#test {to {}}`;
await testProjection(code);
strictEqual(collection.length, 4);
strictEqual(collection[2].kind, "ModelProperty");
strictEqual(collection[3].kind, "Model");
});
it("project property with type referencing sibling", async () => {
const code = `
@test model Foo {
a: Foo.b;
b: string;
}
#suppress "projections-are-experimental"
projection model#test {to {}}`;
const result = (await testProjection(code)) as Model;
ok(result.projectionBase);
strictEqual(result.properties.get("a")?.type, result.properties.get("b"));
});
it("can recursively apply projections to nested models", async () => {
const code = `
@test model Foo {
@ -617,18 +512,6 @@ describe("compiler: projections", () => {
strictEqual((variant.type as Model).name, typeName);
}
it("link projected model to projected properties", async () => {
const code = `
@test union Foo {
one: {};
}
#suppress "projections-are-experimental"
projection model#test {to {}}`;
const result = (await testProjection(code)) as Union;
ok(result.projectionBase);
strictEqual(result.variants.get("one")?.union, result);
});
it("can rename itself", async () => {
const code = `
${unionCode}
@ -645,6 +528,7 @@ describe("compiler: projections", () => {
strictEqual(result.name, "Bar");
strictEqual(result.namespace!.unions.get("Bar"), result);
});
it("can rename variants", async () => {
const code = defaultCode(`
self::variants::forEach((v) => {
@ -755,18 +639,6 @@ describe("compiler: projections", () => {
${projectionCode(body)}
`;
it("link projected interfaces to its projected operations", async () => {
const code = `
@test interface Foo {
op test(): string;
}
#suppress "projections-are-experimental"
projection interface#test {to {}}`;
const result = (await testProjection(code)) as Interface;
ok(result.projectionBase);
strictEqual(result.operations.get("test")?.interface, result);
});
it("can rename itself", async () => {
const code = `
${interfaceCode}
@ -821,14 +693,6 @@ describe("compiler: projections", () => {
${projectionCode(body)}
`;
it("link projected enum to projected members", async () => {
const code = defaultCode("");
const result = (await testProjection(code)) as Enum;
ok(result.projectionBase);
strictEqual(result.members.get("one")?.enum, result);
strictEqual(result.members.get("two")?.enum, result);
});
it("can rename itself", async () => {
const code = `
${enumCode}

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

@ -0,0 +1,293 @@
import { deepStrictEqual, ok, strictEqual } from "assert";
import { DecoratorContext, getTypeName, Namespace, Type } from "../../core/index.js";
import { createProjector } from "../../core/projector.js";
import { createTestHost, createTestRunner } from "../../testing/test-host.js";
import { BasicTestRunner, TestHost } from "../../testing/types.js";
/**
* This test suite checks that projected types are reconstructed just fine.
*/
describe("compiler: projector: Identity", () => {
let host: TestHost;
let runner: BasicTestRunner;
beforeEach(async () => {
host = await createTestHost();
runner = await createTestRunner(host);
});
type IdentifyProjectResult<T extends Type> = {
type: T;
globalNamespace: Namespace;
originalType: T;
/**
* Types collected with the `@collect` decorator in the order they were run.
*/
trackedTypes: Type[];
};
/**
* Project the given code without any projection implementation.
*/
async function projectWithNoChange<K extends Type["kind"], T extends Type & { kind: K }>(
code: string,
kind?: K
): Promise<IdentifyProjectResult<T>> {
const projections = [{ arguments: [], projectionName: "noop" }];
const trackedTypes: Type[] = [];
host.addJsFile("./track.js", {
$track: (_: DecoratorContext, target: Type) => trackedTypes.push(target),
});
const { target } = await runner.compile(`
import "./track.js";
${code}`);
while (trackedTypes.length > 0) {
trackedTypes.pop();
}
ok(target, `Expected to have found a test type tagged with target. Add @test("target")`);
if (kind) {
strictEqual(target.kind, kind);
}
const projector = createProjector(runner.program, projections).projector;
const projectedType = projector.projectedTypes.get(target);
ok(projectedType, `Type ${getTypeName(target)} should have been projected`);
if (kind) {
strictEqual(projectedType.kind, kind);
}
strictEqual(projectedType.projectionBase, target);
strictEqual(projectedType.projectionSource, target);
return {
type: projectedType as T,
globalNamespace: projector.projectedGlobalNamespace!,
originalType: target as T,
trackedTypes,
};
}
type TestDecoratorOrderOptions = {
name: string;
code: string;
ref?: string;
expectedTypes: [Type["kind"], string][];
};
function describeDecoratorOrder({ name, code, ref, expectedTypes }: TestDecoratorOrderOptions) {
if (ref === undefined) {
it(name, async () => {
const emptyCode = `@test("target") model Empty {}`;
const result = await projectWithNoChange(`${emptyCode}\n${code}`);
expectTrackedTypes(result.trackedTypes, expectedTypes);
});
} else {
const refCode = `
@test("target") model Referencing {
b: ${ref};
}
`;
describe(name, () => {
it("referenced before", async () => {
const result = await projectWithNoChange(`${refCode}\n${code}`);
expectTrackedTypes(result.trackedTypes, expectedTypes);
});
it("referenced after", async () => {
const result = await projectWithNoChange(`${code}\n${refCode}`);
expectTrackedTypes(result.trackedTypes, expectedTypes);
});
});
}
}
function expectTrackedTypes(trackedTypes: Type[], expectedTypes: [Type["kind"], string][]) {
deepStrictEqual(
trackedTypes.map((x) => [x.kind, getTypeName(x)]),
expectedTypes
);
}
describe("models", () => {
it("link projected model to projected properties", async () => {
const projectResult = await projectWithNoChange(
`
@test("target") model Foo {
name: string;
}
`,
"Model"
);
strictEqual(projectResult.type.properties.get("name")?.model, projectResult.type);
});
it("link projected property with sourceProperty", async () => {
const code = `
@test("target") model Foo {
...Spreadable
}
model Spreadable {
name: string;
}
`;
const projectResult = await projectWithNoChange(code, "Model");
const sourceProperty = projectResult.type.properties.get("name")?.sourceProperty;
ok(sourceProperty);
const Spreadable = projectResult.globalNamespace.models.get("Spreadable")!;
strictEqual(sourceProperty, Spreadable.properties.get("name"));
});
it("project property with type referencing sibling", async () => {
const code = `
@test("target") model Foo {
a: Foo.b;
b: string;
}`;
const result = await projectWithNoChange(code, "Model");
strictEqual(result.type.properties.get("a")?.type, result.type.properties.get("b"));
});
describe("runs decorator on property before model", () => {
describeDecoratorOrder({
name: "simple",
code: `
@track model Foo {
@track a: string
}`,
ref: "Foo.a",
expectedTypes: [
["ModelProperty", "Foo.a"],
["Model", "Foo"],
],
});
describeDecoratorOrder({
name: "with spread properties",
code: `
@track model Foo {
...Spreadable;
}
model Spreadable {
@track name: string;
}`,
ref: "Foo.name",
expectedTypes: [
["ModelProperty", "Spreadable.name"],
["ModelProperty", "Foo.name"],
["Model", "Foo"],
],
});
});
});
describe("unions", () => {
it("link projected unions to projected variants", async () => {
const projectResult = await projectWithNoChange(
`
@test("target") union Foo {
one: {};
}
`,
"Union"
);
strictEqual(projectResult.type.variants.get("one")?.union, projectResult.type);
});
describe("runs decorator on variants before unions", () => {
describeDecoratorOrder({
name: "simple",
code: `
@track union Foo {
@track one: {}
}`,
ref: "Foo.one",
expectedTypes: [
["UnionVariant", "{}"],
["Union", "Foo"],
],
});
});
});
describe("enum", () => {
it("link projected enum to the projected enum member", async () => {
const projectResult = await projectWithNoChange(
`
@test("target") enum Foo {
one,
}
`,
"Enum"
);
strictEqual(projectResult.type.members.get("one")?.enum, projectResult.type);
});
describe("runs decorator on variants before unions", () => {
describeDecoratorOrder({
name: "simple",
code: `
@track enum Foo {
@track one,
}`,
ref: "Foo.one",
expectedTypes: [
["EnumMember", "Foo.one"],
["Enum", "Foo"],
],
});
describeDecoratorOrder({
name: "with spread members",
code: `
@track enum Foo {
...Spreadable;
}
enum Spreadable {
@track one,
}`,
ref: "Foo.one",
expectedTypes: [
["EnumMember", "Foo.one"],
["Enum", "Foo"],
["EnumMember", "Spreadable.one"],
],
});
});
});
describe("interface", () => {
it("link projected interface to the projected operation member", async () => {
const projectResult = await projectWithNoChange(
`
@test("target") interface Foo {
one(): void;
}
`,
"Interface"
);
strictEqual(projectResult.type.operations.get("one")?.interface, projectResult.type);
});
describe("runs decorator on variants before unions", () => {
describeDecoratorOrder({
name: "simple",
code: `
@track interface Foo {
@track one(): void;
}`,
ref: "Foo.one",
expectedTypes: [
["Operation", "one"],
["Interface", "Foo"],
],
});
});
});
});