Merge pull request #388 from daviwil/service-metadata
This commit is contained in:
Коммит
5f8b5c3ca9
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче