Merge pull request #388 from daviwil/service-metadata

This commit is contained in:
David Wilson 2021-03-29 16:01:45 -07:00 коммит произвёл GitHub
Родитель 0a344db306 dfd77d4898
Коммит 5f8b5c3ca9
6 изменённых файлов: 278 добавлений и 36 удалений

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

@ -1,6 +1,6 @@
import { createDiagnostic, Diagnostic, DiagnosticError, formatDiagnostic } from "./diagnostics.js";
import { visitChildren } from "./parser.js";
import { Program } from "./program.js";
import { createProgram, Program } from "./program.js";
import {
NamespaceStatementNode,
ModelStatementNode,
@ -54,8 +54,9 @@ export interface Binder {
export function createBinder(): Binder {
let currentFile: ADLScriptNode;
let parentNode: Node;
let currentNamespace: NamespaceStatementNode | ADLScriptNode;
let globalNamespace: NamespaceStatementNode;
let fileNamespace: NamespaceStatementNode;
let currentNamespace: NamespaceStatementNode;
// Node where locals go.
let scope: ScopeNode;
@ -64,12 +65,12 @@ export function createBinder(): Binder {
};
function bindSourceFile(program: Program, sourceFile: ADLScriptNode) {
globalNamespace = program.globalNamespace;
fileNamespace = globalNamespace;
currentFile = sourceFile;
currentNamespace = scope = currentFile;
currentFile.locals = new SymbolTable();
currentFile.exports = program.globalNamespace.exports!;
currentNamespace = scope = globalNamespace;
bindNode(sourceFile);
currentFile.inScopeNamespaces.push(globalNamespace);
}
function bindNode(node: Node) {
@ -110,8 +111,6 @@ export function createBinder(): Binder {
visitChildren(node, bindNode);
if (node.kind !== SyntaxKind.NamespaceStatement) {
// we've finished binding all the children, so make sure
// there are no duplicates.
reportDuplicateSymbols(node.locals!);
}
@ -128,8 +127,9 @@ export function createBinder(): Binder {
function getContainingSymbolTable() {
switch (scope.kind) {
case SyntaxKind.NamespaceStatement:
case SyntaxKind.ADLScript:
return scope.exports!;
case SyntaxKind.ADLScript:
return fileNamespace.exports!;
default:
return scope.locals!;
}
@ -147,7 +147,7 @@ export function createBinder(): Binder {
function bindNamespaceStatement(statement: NamespaceStatementNode) {
// check if there's an existing symbol for this namespace
const existingBinding = (scope as NamespaceStatementNode).exports!.get(statement.name.sv);
const existingBinding = currentNamespace.exports!.get(statement.name.sv);
if (existingBinding && existingBinding.kind === "type") {
statement.symbol = existingBinding;
// locals are never shared.
@ -165,8 +165,14 @@ export function createBinder(): Binder {
currentFile.namespaces.push(statement);
if (!statement.statements) {
currentFile.exports = statement.exports!;
if (statement.statements === undefined) {
scope = currentNamespace = statement;
fileNamespace = statement;
let current: ADLScriptNode | NamespaceStatementNode = statement;
while (current.kind !== SyntaxKind.ADLScript) {
currentFile.inScopeNamespaces.push(current);
current = current.parent as ADLScriptNode | NamespaceStatementNode;
}
}
}
@ -189,6 +195,11 @@ export function createBinder(): Binder {
}
node.namespaceSymbol = scope.symbol;
} else if (scope.kind === SyntaxKind.ADLScript) {
if (node.kind === SyntaxKind.TemplateParameterDeclaration) {
throw new Error("Attempted to declare template parameter in global scope");
}
node.namespaceSymbol = fileNamespace.symbol;
}
table.set(name, symbol);
@ -200,7 +211,7 @@ function hasScope(node: Node): node is ScopeNode {
case SyntaxKind.ModelStatement:
return true;
case SyntaxKind.NamespaceStatement:
return true;
return node.statements !== undefined;
case SyntaxKind.ADLScript:
return true;
default:

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

@ -124,6 +124,7 @@ export function createChecker(program: Program) {
checkProgram,
getLiteralType,
getTypeName,
getNamespaceString,
checkOperation,
};
@ -478,31 +479,34 @@ export function createChecker(program: Program) {
function resolveIdentifier(node: IdentifierNode) {
let scope: Node | undefined = node.parent;
let binding;
let containerSourceFile: ADLScriptNode;
while (scope) {
while (scope && scope.kind !== SyntaxKind.ADLScript) {
if ("exports" in scope) {
binding = resolveIdentifierInTable(node, scope.exports!);
if (binding) break;
if (binding) return binding;
}
if ("locals" in scope) {
binding = resolveIdentifierInTable(node, scope.locals!);
if (binding) break;
if (binding) return binding;
}
scope = scope.parent;
}
if (!binding) {
binding = resolveIdentifierInTable(node, program.globalNamespace.exports!);
if (!binding && scope && scope.kind === SyntaxKind.ADLScript) {
// check any blockless namespace decls and global scope
for (const ns of scope.inScopeNamespaces) {
binding = resolveIdentifierInTable(node, ns.exports!);
if (binding) return binding;
}
// check "global scope" usings
binding = resolveIdentifierInTable(node, scope.locals);
if (binding) return binding;
}
if (!binding) {
throwDiagnostic("Unknown identifier " + node.sv, node);
}
return binding;
throwDiagnostic("Unknown identifier " + node.sv, node);
}
function resolveTypeReference(node: ReferenceExpression): DecoratorSymbol | TypeSymbol {

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

@ -1,3 +1,4 @@
import { SymbolTable } from "./binder.js";
import { throwDiagnostic } from "./diagnostics.js";
import { createScanner, Token } from "./scanner.js";
import * as Types from "./types.js";
@ -19,6 +20,8 @@ export function parse(code: string | Types.SourceFile) {
models: [],
namespaces: [],
usings: [],
locals: new SymbolTable(),
inScopeNamespaces: [],
};
let seenBlocklessNs = false;

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

@ -191,9 +191,9 @@ export interface ADLScriptNode extends BaseNode {
models: Array<ModelType>;
file: SourceFile;
interfaces: Array<NamespaceType>;
locals?: SymbolTable;
exports?: SymbolTable;
inScopeNamespaces: NamespaceStatementNode[]; // namespaces that declarations in this file belong to
namespaces: NamespaceStatementNode[]; // list of namespaces in this file (initialized during binding)
locals: SymbolTable;
usings: UsingStatementNode[];
}

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

@ -1,5 +1,5 @@
import { Program } from "../compiler/program";
import { Type } from "../compiler/types";
import { NamespaceType, Type } from "../compiler/types";
const basePaths = new Map<Type, string>();
@ -125,3 +125,86 @@ export function _delete(program: Program, entity: Type, subPath?: string) {
subPath,
});
}
// -- Service-level Metadata
let _serviceTitle: { type: NamespaceType; title: string } | undefined = undefined;
export function serviceTitle(program: Program, entity: Type, title: string) {
if (_serviceTitle && _serviceTitle.type !== entity) {
throw new Error("Service title can only be set once per ADL document.");
}
if (entity.kind !== "Namespace") {
throw new Error("The @serviceTitle decorator can only be applied to namespaces.");
}
_serviceTitle = {
type: entity,
title,
};
}
export function getServiceTitle(): string {
return _serviceTitle ? _serviceTitle.title : "(title)";
}
let _serviceVersion: { type: NamespaceType; version: string } | undefined = undefined;
export function serviceVersion(program: Program, entity: Type, version: string) {
// TODO: This will need to change once we support multiple service versions
if (_serviceVersion && _serviceVersion.type !== entity) {
throw new Error("Service version can only be set once per ADL document.");
}
if (entity.kind !== "Namespace") {
throw new Error("The @serviceVersion decorator can only be applied to namespaces.");
}
_serviceVersion = {
type: entity,
version,
};
}
export function getServiceVersion(): string {
return _serviceVersion ? _serviceVersion.version : "0000-00-00";
}
export function detectServiceNamespace(program: Program): string | undefined {
return (
(_serviceTitle && program.checker!.getNamespaceString(_serviceTitle.type)) ||
(_serviceVersion && program.checker!.getNamespaceString(_serviceVersion.type)) ||
undefined
);
}
const producesTypes = new Map<Type, string[]>();
export function produces(program: Program, entity: Type, ...contentTypes: string[]) {
if (entity.kind !== "Namespace") {
throw new Error("The @produces decorator can only be applied to namespaces.");
}
const values = getProduces(entity);
producesTypes.set(entity, values.concat(contentTypes));
}
export function getProduces(entity: Type): string[] {
return producesTypes.get(entity) || [];
}
const consumesTypes = new Map<Type, string[]>();
export function consumes(program: Program, entity: Type, ...contentTypes: string[]) {
if (entity.kind !== "Namespace") {
throw new Error("The @consumes decorator can only be applied to namespaces.");
}
const values = getConsumes(entity);
consumesTypes.set(entity, values.concat(contentTypes));
}
export function getConsumes(entity: Type): string[] {
return consumesTypes.get(entity) || [];
}

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

@ -39,15 +39,21 @@ describe("namespaces with blocks", () => {
testHost.addAdlFile(
"a.adl",
`
namespace N { model X { x: string } }
namespace N { model Y { y: string } }
@test
namespace N { @test model X { x: string } }
namespace N { @test model Y { y: string } }
namespace N { @test model Z { ... X, ... Y } }
`
);
const { Z } = (await testHost.compile("./")) as {
const { N, X, Y, Z } = (await testHost.compile("./")) as {
N: NamespaceType;
X: ModelType;
Y: ModelType;
Z: ModelType;
};
strictEqual(X.namespace, N);
strictEqual(Y.namespace, N);
strictEqual(Z.namespace, N);
strictEqual(Z.properties.size, 2, "has two properties");
});
@ -55,13 +61,14 @@ describe("namespaces with blocks", () => {
testHost.addAdlFile(
"a.adl",
`
namespace N { model X { x: string } }
@test
namespace N { @test model X { x: string } }
`
);
testHost.addAdlFile(
"b.adl",
`
namespace N { model Y { y: int32 } }
namespace N { @test model Y { y: int32 } }
`
);
testHost.addAdlFile(
@ -70,9 +77,15 @@ describe("namespaces with blocks", () => {
namespace N { @test model Z { ... X, ... Y } }
`
);
const { Z } = (await testHost.compile("./")) as {
const { N, X, Y, Z } = (await testHost.compile("./")) as {
N: NamespaceType;
X: ModelType;
Y: ModelType;
Z: ModelType;
};
strictEqual(X.namespace, N, "X namespace");
strictEqual(Y.namespace, N, "Y namespace");
strictEqual(Z.namespace, N, "Z namespace");
strictEqual(Z.properties.size, 2, "has two properties");
});
@ -101,6 +114,42 @@ describe("namespaces with blocks", () => {
};
strictEqual(Z.properties.size, 2, "has two properties");
});
it("can see things in outer scope same file", async () => {
testHost.addAdlFile(
"a.adl",
`
model A { }
namespace N { model B extends A { } }
`
);
await testHost.compile("./");
});
it("can see things in outer scope cross file", async () => {
testHost.addAdlFile(
"a.adl",
`
model A { }
`
);
testHost.addAdlFile(
"b.adl",
`
model B extends A { }
`
);
testHost.addAdlFile(
"c.adl",
`
model C { }
namespace foo {
op foo(a: A, b: B): C;
}
`
);
await testHost.compile("./");
});
});
describe("blockless namespaces", () => {
@ -155,7 +204,42 @@ describe("blockless namespaces", () => {
}
`
);
await testHost.compile("./");
try {
await testHost.compile("./");
} catch (e) {
console.log(e.diagnostics);
throw e;
}
});
it("does lookup correctly with nested namespaces", async () => {
testHost.addAdlFile(
"a.adl",
`
namespace Repro;
model Yo {
}
model Hey {
wat: Yo;
}
`
);
testHost.addAdlFile(
"b.adl",
`
namespace Repro.Uhoh;
model SayYo {
yo: Hey;
wat: Yo;
}
`
);
try {
await testHost.compile("./");
} catch (e) {
console.log(e.diagnostics);
throw e;
}
});
it("binds correctly", async () => {
@ -179,4 +263,61 @@ describe("blockless namespaces", () => {
throw e;
}
});
it("works with blockful namespaces", async () => {
testHost.addAdlFile(
"a.adl",
`
@test
namespace N;
@test
namespace M {
model A { }
}
`
);
testHost.addAdlFile(
"b.adl",
`
model X { a: N.M.A }
`
);
const { N, M } = (await testHost.compile("/")) as {
N: NamespaceType;
M: NamespaceType;
};
ok(M.namespace);
strictEqual(M.namespace, N);
});
it("works with nested blockless and blockfull namespaces", async () => {
testHost.addAdlFile(
"a.adl",
`
@test
namespace N.M;
@test
namespace O {
model A { }
}
`
);
testHost.addAdlFile(
"b.adl",
`
model X { a: N.M.O.A }
`
);
const { M, O } = (await testHost.compile("/")) as {
M: NamespaceType;
O: NamespaceType;
};
ok(M.namespace);
ok(O.namespace);
strictEqual(O.namespace, M);
});
});