Implement alias and enum, remove model = (#504)
This commit is contained in:
Родитель
051862aa54
Коммит
8c86e04427
|
@ -206,6 +206,8 @@ Statement :
|
|||
NamespaceStatement
|
||||
OperationStatement
|
||||
UsingStatement
|
||||
EnumStatement
|
||||
AliasStatement
|
||||
`;`
|
||||
|
||||
UsingStatement :
|
||||
|
@ -213,22 +215,10 @@ UsingStatement :
|
|||
|
||||
ModelStatement :
|
||||
DecoratorList? `model` Identifier TemplateParameters? ModelHeritage? `{` ModelBody? `}`
|
||||
DecoratorList? `model` Identifier TemplateParameters? `=` Expression `;`
|
||||
|
||||
ModelHeritage :
|
||||
`extends` ReferenceExpressionList
|
||||
|
||||
ReferenceExpressionList :
|
||||
ReferenceExpression
|
||||
ReferenceExpressionList `,` ReferenceExpression
|
||||
|
||||
TemplateParameters :
|
||||
`<` IdentifierList `>`
|
||||
|
||||
IdentifierList :
|
||||
Identifier
|
||||
IdentifierList `,` Identifier
|
||||
|
||||
ModelBody :
|
||||
ModelPropertyList `,`?
|
||||
ModelPropertyList `;`?
|
||||
|
@ -246,6 +236,40 @@ ModelProperty:
|
|||
ModelSpreadProperty :
|
||||
`...` ReferenceExpression
|
||||
|
||||
EnumStatement :
|
||||
DecoratorList? `enum` Identifier `{` EnumBody? `}`
|
||||
|
||||
EnumBody :
|
||||
EnumMemberList `,`?
|
||||
EnumMemberList `;`?
|
||||
|
||||
EnumMemberList :
|
||||
EnumMember
|
||||
EnumMemberList `,` EnumMember
|
||||
EnumMemberList `;` EnumMember
|
||||
|
||||
EnumMember :
|
||||
DecoratorList? Identifier EnumMemberValue?
|
||||
DecoratorList? StringLiteral EnumMemberValue?
|
||||
|
||||
EnumMemberValue :
|
||||
`:` StringLiteral
|
||||
`:` NumericLiteral
|
||||
|
||||
AliasStatement :
|
||||
`alias` Identifier TemplateParameters? `=` Expression;
|
||||
|
||||
ReferenceExpressionList :
|
||||
ReferenceExpression
|
||||
ReferenceExpressionList `,` ReferenceExpression
|
||||
|
||||
TemplateParameters :
|
||||
`<` IdentifierList `>`
|
||||
|
||||
IdentifierList :
|
||||
Identifier
|
||||
IdentifierList `,` Identifier
|
||||
|
||||
NamespaceStatement:
|
||||
DecoratorList? `namespace` IdentifierOrMemberExpression `{` StatementList? `}`
|
||||
|
||||
|
|
|
@ -1,67 +1,66 @@
|
|||
import { NamespaceType, Program, throwDiagnostic, Type } from "@azure-tools/adl";
|
||||
|
||||
const basePaths = new Map<Type, string>();
|
||||
|
||||
const basePathsKey = Symbol();
|
||||
export function resource(program: Program, entity: Type, basePath = "") {
|
||||
if (entity.kind !== "Namespace") return;
|
||||
basePaths.set(entity, basePath);
|
||||
program.stateMap(basePathsKey).set(entity, basePath);
|
||||
}
|
||||
|
||||
export function getResources() {
|
||||
return Array.from(basePaths.keys());
|
||||
export function getResources(program: Program) {
|
||||
return Array.from(program.stateMap(basePathsKey).keys());
|
||||
}
|
||||
|
||||
export function isResource(obj: Type) {
|
||||
return basePaths.has(obj);
|
||||
export function isResource(program: Program, obj: Type) {
|
||||
return program.stateMap(basePathsKey).has(obj);
|
||||
}
|
||||
|
||||
export function basePathForResource(resource: Type) {
|
||||
return basePaths.get(resource);
|
||||
export function basePathForResource(program: Program, resource: Type) {
|
||||
return program.stateMap(basePathsKey).get(resource);
|
||||
}
|
||||
|
||||
const headerFields = new Map<Type, string>();
|
||||
const headerFieldsKey = Symbol();
|
||||
export function header(program: Program, entity: Type, headerName: string) {
|
||||
if (!headerName && entity.kind === "ModelProperty") {
|
||||
headerName = entity.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
}
|
||||
headerFields.set(entity, headerName);
|
||||
program.stateMap(headerFieldsKey).set(entity, headerName);
|
||||
}
|
||||
|
||||
export function getHeaderFieldName(entity: Type) {
|
||||
return headerFields.get(entity);
|
||||
export function getHeaderFieldName(program: Program, entity: Type) {
|
||||
return program.stateMap(headerFieldsKey).get(entity);
|
||||
}
|
||||
|
||||
const queryFields = new Map<Type, string>();
|
||||
const queryFieldsKey = Symbol();
|
||||
export function query(program: Program, entity: Type, queryKey: string) {
|
||||
if (!queryKey && entity.kind === "ModelProperty") {
|
||||
queryKey = entity.name;
|
||||
}
|
||||
queryFields.set(entity, queryKey);
|
||||
program.stateMap(queryFieldsKey).set(entity, queryKey);
|
||||
}
|
||||
|
||||
export function getQueryParamName(entity: Type) {
|
||||
return queryFields.get(entity);
|
||||
export function getQueryParamName(program: Program, entity: Type) {
|
||||
return program.stateMap(queryFieldsKey).get(entity);
|
||||
}
|
||||
|
||||
const pathFields = new Map<Type, string>();
|
||||
const pathFieldsKey = Symbol();
|
||||
export function path(program: Program, entity: Type, paramName: string) {
|
||||
if (!paramName && entity.kind === "ModelProperty") {
|
||||
paramName = entity.name;
|
||||
}
|
||||
pathFields.set(entity, paramName);
|
||||
program.stateMap(pathFieldsKey).set(entity, paramName);
|
||||
}
|
||||
|
||||
export function getPathParamName(entity: Type) {
|
||||
return pathFields.get(entity);
|
||||
export function getPathParamName(program: Program, entity: Type) {
|
||||
return program.stateMap(pathFieldsKey).get(entity);
|
||||
}
|
||||
|
||||
const bodyFields = new Set<Type>();
|
||||
const bodyFieldsKey = Symbol();
|
||||
export function body(program: Program, entity: Type) {
|
||||
bodyFields.add(entity);
|
||||
program.stateSet(bodyFieldsKey).add(entity);
|
||||
}
|
||||
|
||||
export function isBody(entity: Type) {
|
||||
return bodyFields.has(entity);
|
||||
export function isBody(program: Program, entity: Type) {
|
||||
return program.stateSet(bodyFieldsKey).has(entity);
|
||||
}
|
||||
|
||||
export type HttpVerb = "get" | "put" | "post" | "patch" | "delete";
|
||||
|
@ -71,12 +70,12 @@ interface OperationRoute {
|
|||
subPath?: string;
|
||||
}
|
||||
|
||||
const operationRoutes = new Map<Type, OperationRoute>();
|
||||
const operationRoutesKey = Symbol();
|
||||
|
||||
function setOperationRoute(entity: Type, verb: OperationRoute) {
|
||||
function setOperationRoute(program: Program, entity: Type, verb: OperationRoute) {
|
||||
if (entity.kind === "Operation") {
|
||||
if (!operationRoutes.has(entity)) {
|
||||
operationRoutes.set(entity, verb);
|
||||
if (!program.stateMap(operationRoutesKey).has(entity)) {
|
||||
program.stateMap(operationRoutesKey).set(entity, verb);
|
||||
} else {
|
||||
throwDiagnostic(`HTTP verb already applied to ${entity.name}`, entity);
|
||||
}
|
||||
|
@ -85,33 +84,33 @@ function setOperationRoute(entity: Type, verb: OperationRoute) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getOperationRoute(entity: Type): OperationRoute | undefined {
|
||||
return operationRoutes.get(entity);
|
||||
export function getOperationRoute(program: Program, entity: Type): OperationRoute | undefined {
|
||||
return program.stateMap(operationRoutesKey).get(entity);
|
||||
}
|
||||
|
||||
export function get(program: Program, entity: Type, subPath?: string) {
|
||||
setOperationRoute(entity, {
|
||||
setOperationRoute(program, entity, {
|
||||
verb: "get",
|
||||
subPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function put(program: Program, entity: Type, subPath?: string) {
|
||||
setOperationRoute(entity, {
|
||||
setOperationRoute(program, entity, {
|
||||
verb: "put",
|
||||
subPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function post(program: Program, entity: Type, subPath?: string) {
|
||||
setOperationRoute(entity, {
|
||||
setOperationRoute(program, entity, {
|
||||
verb: "post",
|
||||
subPath,
|
||||
});
|
||||
}
|
||||
|
||||
export function patch(program: Program, entity: Type, subPath?: string) {
|
||||
setOperationRoute(entity, {
|
||||
setOperationRoute(program, entity, {
|
||||
verb: "patch",
|
||||
subPath,
|
||||
});
|
||||
|
@ -119,7 +118,7 @@ export function patch(program: Program, entity: Type, subPath?: string) {
|
|||
|
||||
// BUG #243: How do we deal with reserved words?
|
||||
export function _delete(program: Program, entity: Type, subPath?: string) {
|
||||
setOperationRoute(entity, {
|
||||
setOperationRoute(program, entity, {
|
||||
verb: "delete",
|
||||
subPath,
|
||||
});
|
||||
|
@ -127,13 +126,24 @@ export function _delete(program: Program, entity: Type, subPath?: string) {
|
|||
|
||||
// -- Service-level Metadata
|
||||
|
||||
const serviceDetails: {
|
||||
interface ServiceDetails {
|
||||
namespace?: NamespaceType;
|
||||
title?: string;
|
||||
version?: string;
|
||||
} = {};
|
||||
}
|
||||
const programServiceDetails = new WeakMap<Program, ServiceDetails>();
|
||||
function getServiceDetails(program: Program) {
|
||||
let serviceDetails = programServiceDetails.get(program);
|
||||
if (!serviceDetails) {
|
||||
serviceDetails = {};
|
||||
programServiceDetails.set(program, serviceDetails);
|
||||
}
|
||||
|
||||
export function _setServiceNamespace(namespace: NamespaceType): void {
|
||||
return serviceDetails;
|
||||
}
|
||||
|
||||
export function _setServiceNamespace(program: Program, namespace: NamespaceType): void {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
if (serviceDetails.namespace && serviceDetails.namespace !== namespace) {
|
||||
throwDiagnostic("Cannot set service namespace more than once in an ADL project.", namespace);
|
||||
}
|
||||
|
@ -141,11 +151,13 @@ export function _setServiceNamespace(namespace: NamespaceType): void {
|
|||
serviceDetails.namespace = namespace;
|
||||
}
|
||||
|
||||
export function _checkIfServiceNamespace(namespace: NamespaceType): boolean {
|
||||
export function _checkIfServiceNamespace(program: Program, namespace: NamespaceType): boolean {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
return serviceDetails.namespace === namespace;
|
||||
}
|
||||
|
||||
export function serviceTitle(program: Program, entity: Type, title: string) {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
if (serviceDetails.title) {
|
||||
throwDiagnostic("Service title can only be set once per ADL document.", entity);
|
||||
}
|
||||
|
@ -154,15 +166,17 @@ export function serviceTitle(program: Program, entity: Type, title: string) {
|
|||
throwDiagnostic("The @serviceTitle decorator can only be applied to namespaces.", entity);
|
||||
}
|
||||
|
||||
_setServiceNamespace(entity);
|
||||
_setServiceNamespace(program, entity);
|
||||
serviceDetails.title = title;
|
||||
}
|
||||
|
||||
export function getServiceTitle(): string {
|
||||
export function getServiceTitle(program: Program): string {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
return serviceDetails.title || "(title)";
|
||||
}
|
||||
|
||||
export function serviceVersion(program: Program, entity: Type, version: string) {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
// TODO: This will need to change once we support multiple service versions
|
||||
if (serviceDetails.version) {
|
||||
throwDiagnostic("Service version can only be set once per ADL document.", entity);
|
||||
|
@ -172,47 +186,49 @@ export function serviceVersion(program: Program, entity: Type, version: string)
|
|||
throwDiagnostic("The @serviceVersion decorator can only be applied to namespaces.", entity);
|
||||
}
|
||||
|
||||
_setServiceNamespace(entity);
|
||||
_setServiceNamespace(program, entity);
|
||||
serviceDetails.version = version;
|
||||
}
|
||||
|
||||
export function getServiceVersion(): string {
|
||||
export function getServiceVersion(program: Program): string {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
return serviceDetails.version || "0000-00-00";
|
||||
}
|
||||
|
||||
export function getServiceNamespaceString(program: Program): string | undefined {
|
||||
const serviceDetails = getServiceDetails(program);
|
||||
return (
|
||||
(serviceDetails.namespace && program.checker!.getNamespaceString(serviceDetails.namespace)) ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
const producesTypes = new Map<Type, string[]>();
|
||||
const producesTypesKey = Symbol();
|
||||
|
||||
export function produces(program: Program, entity: Type, ...contentTypes: string[]) {
|
||||
if (entity.kind !== "Namespace") {
|
||||
throwDiagnostic("The @produces decorator can only be applied to namespaces.", entity);
|
||||
}
|
||||
|
||||
const values = getProduces(entity);
|
||||
producesTypes.set(entity, values.concat(contentTypes));
|
||||
const values = getProduces(program, entity);
|
||||
program.stateMap(producesTypesKey).set(entity, values.concat(contentTypes));
|
||||
}
|
||||
|
||||
export function getProduces(entity: Type): string[] {
|
||||
return producesTypes.get(entity) || [];
|
||||
export function getProduces(program: Program, entity: Type): string[] {
|
||||
return program.stateMap(producesTypesKey).get(entity) || [];
|
||||
}
|
||||
|
||||
const consumesTypes = new Map<Type, string[]>();
|
||||
const consumesTypesKey = Symbol();
|
||||
|
||||
export function consumes(program: Program, entity: Type, ...contentTypes: string[]) {
|
||||
if (entity.kind !== "Namespace") {
|
||||
throwDiagnostic("The @consumes decorator can only be applied to namespaces.", entity);
|
||||
}
|
||||
|
||||
const values = getConsumes(entity);
|
||||
consumesTypes.set(entity, values.concat(contentTypes));
|
||||
const values = getConsumes(program, entity);
|
||||
program.stateMap(consumesTypesKey).set(entity, values.concat(contentTypes));
|
||||
}
|
||||
|
||||
export function getConsumes(entity: Type): string[] {
|
||||
return consumesTypes.get(entity) || [];
|
||||
export function getConsumes(program: Program, entity: Type): string[] {
|
||||
return program.stateMap(consumesTypesKey).get(entity) || [];
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ const identifierContinue = "[_$[:alnum:]]";
|
|||
const beforeIdentifier = `(?=${identifierStart})`;
|
||||
const identifier = `\\b${identifierStart}${identifierContinue}*\\b`;
|
||||
const stringPattern = '\\"(?:[^\\"\\\\]|\\\\.)*\\"';
|
||||
const statementKeyword = `\\b(?:namespace|model|op|using|import)\\b`;
|
||||
const statementKeyword = `\\b(?:namespace|model|op|using|import|enum|alias)\\b`;
|
||||
const universalEnd = `(?=,|;|@|\\)|\\}|${statementKeyword})`;
|
||||
const hexNumber = "\\b(?<!\\$)0(?:x|X)[0-9a-fA-F][0-9a-fA-F_]*(n)?\\b(?!\\$)";
|
||||
const binaryNumber = "\\b(?<!\\$)0(?:b|B)[01][01_]*(n)?\\b(?!\\$)";
|
||||
|
@ -210,6 +210,28 @@ const modelStatement: BeginEndRule = {
|
|||
],
|
||||
};
|
||||
|
||||
const enumStatement: BeginEndRule = {
|
||||
key: "enum-statement",
|
||||
scope: meta,
|
||||
begin: "\\b(enum)\\b",
|
||||
beginCaptures: {
|
||||
"1": { scope: "keyword.other.adl" },
|
||||
},
|
||||
end: `(?<=\\})|${universalEnd}`,
|
||||
patterns: [token, expression],
|
||||
};
|
||||
|
||||
const aliasStatement: BeginEndRule = {
|
||||
key: "alias-statement",
|
||||
scope: meta,
|
||||
begin: "\\b(alias)\\b",
|
||||
beginCaptures: {
|
||||
"1": { scope: "keyword.other.adl" },
|
||||
},
|
||||
end: universalEnd,
|
||||
patterns: [token, expression],
|
||||
};
|
||||
|
||||
const namespaceName: BeginEndRule = {
|
||||
key: "namespace-name",
|
||||
scope: meta,
|
||||
|
@ -316,6 +338,8 @@ statement.patterns = [
|
|||
token,
|
||||
decorator,
|
||||
modelStatement,
|
||||
enumStatement,
|
||||
aliasStatement,
|
||||
namespaceStatement,
|
||||
operationStatement,
|
||||
importStatement,
|
||||
|
|
|
@ -3,7 +3,9 @@ import { visitChildren } from "./parser.js";
|
|||
import { Program } from "./program.js";
|
||||
import {
|
||||
ADLScriptNode,
|
||||
AliasStatementNode,
|
||||
Declaration,
|
||||
EnumStatementNode,
|
||||
ModelStatementNode,
|
||||
NamespaceStatementNode,
|
||||
Node,
|
||||
|
@ -85,6 +87,12 @@ export function createBinder(): Binder {
|
|||
case SyntaxKind.ModelStatement:
|
||||
bindModelStatement(node);
|
||||
break;
|
||||
case SyntaxKind.AliasStatement:
|
||||
bindAliasStatement(node);
|
||||
break;
|
||||
case SyntaxKind.EnumStatement:
|
||||
bindEnumStatement(node);
|
||||
break;
|
||||
case SyntaxKind.NamespaceStatement:
|
||||
bindNamespaceStatement(node);
|
||||
break;
|
||||
|
@ -148,6 +156,16 @@ export function createBinder(): Binder {
|
|||
node.locals = new SymbolTable();
|
||||
}
|
||||
|
||||
function bindAliasStatement(node: AliasStatementNode) {
|
||||
declareSymbol(getContainingSymbolTable(), node, node.id.sv);
|
||||
// Initialize locals for type parameters
|
||||
node.locals = new SymbolTable();
|
||||
}
|
||||
|
||||
function bindEnumStatement(node: EnumStatementNode) {
|
||||
declareSymbol(getContainingSymbolTable(), node, node.id.sv);
|
||||
}
|
||||
|
||||
function bindNamespaceStatement(statement: NamespaceStatementNode) {
|
||||
// check if there's an existing symbol for this namespace
|
||||
const existingBinding = currentNamespace.exports!.get(statement.name.sv);
|
||||
|
@ -217,6 +235,7 @@ export function createBinder(): Binder {
|
|||
function hasScope(node: Node): node is ScopeNode {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.ModelStatement:
|
||||
case SyntaxKind.AliasStatement:
|
||||
return true;
|
||||
case SyntaxKind.NamespaceStatement:
|
||||
return node.statements !== undefined;
|
||||
|
|
|
@ -2,10 +2,15 @@ import { compilerAssert, throwDiagnostic } from "./diagnostics.js";
|
|||
import { Program } from "./program.js";
|
||||
import {
|
||||
ADLScriptNode,
|
||||
AliasStatementNode,
|
||||
ArrayExpressionNode,
|
||||
BooleanLiteralNode,
|
||||
BooleanLiteralType,
|
||||
DecoratorSymbol,
|
||||
EnumMemberNode,
|
||||
EnumMemberType,
|
||||
EnumStatementNode,
|
||||
EnumType,
|
||||
IdentifierNode,
|
||||
IntersectionExpressionNode,
|
||||
IntrinsicType,
|
||||
|
@ -131,6 +136,10 @@ export function createChecker(program: Program) {
|
|||
return checkModel(node);
|
||||
case SyntaxKind.ModelProperty:
|
||||
return checkModelProperty(node);
|
||||
case SyntaxKind.AliasStatement:
|
||||
return checkAlias(node);
|
||||
case SyntaxKind.EnumStatement:
|
||||
return checkEnum(node);
|
||||
case SyntaxKind.NamespaceStatement:
|
||||
return checkNamespace(node);
|
||||
case SyntaxKind.OperationStatement:
|
||||
|
@ -162,6 +171,8 @@ export function createChecker(program: Program) {
|
|||
switch (type.kind) {
|
||||
case "Model":
|
||||
return getModelName(type);
|
||||
case "Enum":
|
||||
return getEnumName(type);
|
||||
case "Union":
|
||||
return type.options.map(getTypeName).join(" | ");
|
||||
case "Array":
|
||||
|
@ -182,6 +193,11 @@ export function createChecker(program: Program) {
|
|||
return parent ? `${getNamespaceString(parent)}.${type.name}` : type.name;
|
||||
}
|
||||
|
||||
function getEnumName(e: EnumType): string {
|
||||
const nsName = getNamespaceString(e.namespace);
|
||||
return nsName ? `${nsName}.${e.name}` : e.name;
|
||||
}
|
||||
|
||||
function getModelName(model: ModelType) {
|
||||
const nsName = getNamespaceString(model.namespace);
|
||||
const modelName = (nsName ? nsName + "." : "") + (model.name || "(anonymous model)");
|
||||
|
@ -222,7 +238,10 @@ export function createChecker(program: Program) {
|
|||
const symbolLinks = getSymbolLinks(sym);
|
||||
const args = node.arguments.map(getTypeForNode);
|
||||
|
||||
if (sym.node.kind === SyntaxKind.ModelStatement && !sym.node.assignment) {
|
||||
if (
|
||||
sym.node.kind === SyntaxKind.ModelStatement ||
|
||||
sym.node.kind === SyntaxKind.AliasStatement
|
||||
) {
|
||||
// model statement, possibly templated
|
||||
if (sym.node.templateParameters.length === 0) {
|
||||
if (args.length > 0) {
|
||||
|
@ -235,14 +254,19 @@ export function createChecker(program: Program) {
|
|||
return pendingModelType.type;
|
||||
}
|
||||
|
||||
return checkModelStatement(sym.node);
|
||||
return sym.node.kind === SyntaxKind.ModelStatement
|
||||
? checkModelStatement(sym.node)
|
||||
: checkAlias(sym.node);
|
||||
} else {
|
||||
// model is templated, lets instantiate.
|
||||
// declaration is templated, lets instantiate.
|
||||
|
||||
if (!symbolLinks.declaredType) {
|
||||
// we haven't checked the declared type yet, so do so.
|
||||
checkModelStatement(sym.node);
|
||||
sym.node.kind === SyntaxKind.ModelStatement
|
||||
? checkModelStatement(sym.node)
|
||||
: checkAlias(sym.node);
|
||||
}
|
||||
|
||||
if (sym.node.templateParameters!.length > node.arguments.length) {
|
||||
throwDiagnostic("Too few template arguments provided.", node);
|
||||
}
|
||||
|
@ -285,7 +309,10 @@ export function createChecker(program: Program) {
|
|||
* twice at the same time, or if template parameters from more than one template
|
||||
* are ever in scope at once.
|
||||
*/
|
||||
function instantiateTemplate(templateNode: ModelStatementNode, args: Type[]): ModelType {
|
||||
function instantiateTemplate(
|
||||
templateNode: ModelStatementNode | AliasStatementNode,
|
||||
args: Type[]
|
||||
): Type {
|
||||
const symbolLinks = getSymbolLinks(templateNode.symbol!);
|
||||
const cached = symbolLinks.instantiations!.get(args) as ModelType;
|
||||
if (cached) {
|
||||
|
@ -296,22 +323,31 @@ export function createChecker(program: Program) {
|
|||
const oldTemplate = instantiatingTemplate;
|
||||
templateInstantiation = args;
|
||||
instantiatingTemplate = templateNode;
|
||||
// this cast is invalid once we support templatized `model =`.
|
||||
const type = getTypeForNode(templateNode) as ModelType;
|
||||
|
||||
const type = getTypeForNode(templateNode);
|
||||
|
||||
symbolLinks.instantiations!.set(args, type);
|
||||
|
||||
type.templateNode = templateNode;
|
||||
if (type.kind === "Model") {
|
||||
type.templateNode = templateNode;
|
||||
}
|
||||
templateInstantiation = oldTis;
|
||||
instantiatingTemplate = oldTemplate;
|
||||
return type;
|
||||
}
|
||||
|
||||
function checkUnionExpression(node: UnionExpressionNode): UnionType {
|
||||
const options = node.options.flatMap((o) => {
|
||||
const type = getTypeForNode(o);
|
||||
if (type.kind === "Union") {
|
||||
return type.options;
|
||||
}
|
||||
return type;
|
||||
});
|
||||
|
||||
return createType({
|
||||
kind: "Union",
|
||||
node,
|
||||
options: node.options.map(getTypeForNode),
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -426,7 +462,7 @@ export function createChecker(program: Program) {
|
|||
}
|
||||
|
||||
function getParentNamespaceType(
|
||||
node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode
|
||||
node: ModelStatementNode | NamespaceStatementNode | OperationStatementNode | EnumStatementNode
|
||||
): NamespaceType | undefined {
|
||||
if (!node.namespaceSymbol) return undefined;
|
||||
|
||||
|
@ -578,11 +614,7 @@ export function createChecker(program: Program) {
|
|||
|
||||
function checkModel(node: ModelExpressionNode | ModelStatementNode) {
|
||||
if (node.kind === SyntaxKind.ModelStatement) {
|
||||
if (node.properties) {
|
||||
return checkModelStatement(node);
|
||||
} else {
|
||||
return checkModelEquals(node);
|
||||
}
|
||||
return checkModelStatement(node);
|
||||
} else {
|
||||
return checkModelExpression(node);
|
||||
}
|
||||
|
@ -672,27 +704,6 @@ export function createChecker(program: Program) {
|
|||
return properties;
|
||||
}
|
||||
|
||||
function checkModelEquals(node: ModelStatementNode) {
|
||||
// model =
|
||||
// this will likely have to change, as right now `model =` is really just
|
||||
// alias and so disappears. That means you can't easily rename symbols.
|
||||
const assignmentType = getTypeForNode(node.assignment!);
|
||||
|
||||
if (assignmentType.kind === "Model") {
|
||||
const type: ModelType = createType({
|
||||
...assignmentType,
|
||||
node: node,
|
||||
name: node.id.sv,
|
||||
assignmentType,
|
||||
namespace: getParentNamespaceType(node),
|
||||
});
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
return assignmentType;
|
||||
}
|
||||
|
||||
function checkClassHeritage(heritage: ReferenceExpression[]): ModelType[] {
|
||||
return heritage.map((heritageRef) => {
|
||||
const heritageType = getTypeForNode(heritageRef);
|
||||
|
@ -761,6 +772,57 @@ export function createChecker(program: Program) {
|
|||
}
|
||||
}
|
||||
|
||||
function checkAlias(node: AliasStatementNode): Type {
|
||||
const links = getSymbolLinks(node.symbol!);
|
||||
const instantiatingThisTemplate = instantiatingTemplate === node;
|
||||
|
||||
if (links.declaredType && !instantiatingThisTemplate) {
|
||||
return links.declaredType;
|
||||
}
|
||||
|
||||
const type = getTypeForNode(node.value);
|
||||
if (!instantiatingThisTemplate) {
|
||||
links.declaredType = type;
|
||||
links.instantiations = new TypeInstantiationMap();
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
function checkEnum(node: EnumStatementNode): Type {
|
||||
const links = getSymbolLinks(node.symbol!);
|
||||
|
||||
if (!links.type) {
|
||||
const enumType: EnumType = {
|
||||
kind: "Enum",
|
||||
name: node.id.sv,
|
||||
node,
|
||||
members: [],
|
||||
namespace: getParentNamespaceType(node),
|
||||
};
|
||||
|
||||
node.members.map((m) => enumType.members.push(checkEnumMember(enumType, m)));
|
||||
|
||||
createType(enumType);
|
||||
|
||||
links.type = enumType;
|
||||
}
|
||||
|
||||
return links.type;
|
||||
}
|
||||
|
||||
function checkEnumMember(parentEnum: EnumType, node: EnumMemberNode): EnumMemberType {
|
||||
const name = node.id.kind === SyntaxKind.Identifier ? node.id.sv : node.id.value;
|
||||
const value = node.value ? node.value.value : undefined;
|
||||
return createType({
|
||||
kind: "EnumMember",
|
||||
enum: parentEnum,
|
||||
name,
|
||||
node,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
// the types here aren't ideal and could probably be refactored.
|
||||
function createType<T extends Type>(typeDef: T): T {
|
||||
(typeDef as any).templateArguments = templateInstantiation;
|
||||
|
@ -772,6 +834,7 @@ export function createChecker(program: Program) {
|
|||
function getLiteralType(node: StringLiteralNode): StringLiteralType;
|
||||
function getLiteralType(node: NumericLiteralNode): NumericLiteralType;
|
||||
function getLiteralType(node: BooleanLiteralNode): BooleanLiteralType;
|
||||
function getLiteralType(node: LiteralNode): LiteralType;
|
||||
function getLiteralType(node: LiteralNode): LiteralType {
|
||||
let type = program.literalTypes.get(node.value);
|
||||
if (type) {
|
||||
|
|
|
@ -11,10 +11,13 @@ import {
|
|||
} from "./scanner.js";
|
||||
import {
|
||||
ADLScriptNode,
|
||||
AliasStatementNode,
|
||||
BooleanLiteralNode,
|
||||
DecoratorExpressionNode,
|
||||
Diagnostic,
|
||||
EmptyStatementNode,
|
||||
EnumMemberNode,
|
||||
EnumStatementNode,
|
||||
Expression,
|
||||
IdentifierNode,
|
||||
ImportStatementNode,
|
||||
|
@ -127,6 +130,10 @@ namespace ListKind {
|
|||
toleratedDelimiter: Token.Comma,
|
||||
} as const;
|
||||
|
||||
export const EnumMembers = {
|
||||
...ModelProperties,
|
||||
} as const;
|
||||
|
||||
const ExpresionsBase = {
|
||||
allowEmpty: true,
|
||||
delimiter: Token.Comma,
|
||||
|
@ -169,7 +176,6 @@ export function parse(code: string | SourceFile) {
|
|||
let missingIdentifierCounter = 0;
|
||||
const parseDiagnostics: Diagnostic[] = [];
|
||||
const scanner = createScanner(code, reportDiagnostic);
|
||||
|
||||
nextToken();
|
||||
return parseADLScript();
|
||||
|
||||
|
@ -212,6 +218,13 @@ export function parse(code: string | SourceFile) {
|
|||
case Token.OpKeyword:
|
||||
item = parseOperationStatement(decorators);
|
||||
break;
|
||||
case Token.EnumKeyword:
|
||||
item = parseEnumStatement(decorators);
|
||||
break;
|
||||
case Token.AliasKeyword:
|
||||
reportInvalidDecorators(decorators, "alias statement");
|
||||
item = parseAliasStatement();
|
||||
break;
|
||||
case Token.UsingKeyword:
|
||||
reportInvalidDecorators(decorators, "using statement");
|
||||
item = parseUsingStatement();
|
||||
|
@ -275,6 +288,13 @@ export function parse(code: string | SourceFile) {
|
|||
case Token.OpKeyword:
|
||||
stmts.push(parseOperationStatement(decorators));
|
||||
break;
|
||||
case Token.EnumKeyword:
|
||||
stmts.push(parseEnumStatement(decorators));
|
||||
break;
|
||||
case Token.AliasKeyword:
|
||||
reportInvalidDecorators(decorators, "alias statement");
|
||||
stmts.push(parseAliasStatement());
|
||||
break;
|
||||
case Token.UsingKeyword:
|
||||
reportInvalidDecorators(decorators, "using statement");
|
||||
stmts.push(parseUsingStatement());
|
||||
|
@ -402,33 +422,18 @@ export function parse(code: string | SourceFile) {
|
|||
|
||||
expectTokenIsOneOf(Token.OpenBrace, Token.Equals, Token.ExtendsKeyword);
|
||||
|
||||
if (parseOptional(Token.Equals)) {
|
||||
const assignment = parseExpression();
|
||||
parseExpected(Token.Semicolon);
|
||||
const heritage: ReferenceExpression[] = parseOptionalModelHeritage();
|
||||
const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread);
|
||||
|
||||
return {
|
||||
kind: SyntaxKind.ModelStatement,
|
||||
id,
|
||||
heritage: [],
|
||||
templateParameters,
|
||||
assignment,
|
||||
decorators,
|
||||
...finishNode(pos),
|
||||
};
|
||||
} else {
|
||||
const heritage: ReferenceExpression[] = parseOptionalModelHeritage();
|
||||
const properties = parseList(ListKind.ModelProperties, parseModelPropertyOrSpread);
|
||||
|
||||
return {
|
||||
kind: SyntaxKind.ModelStatement,
|
||||
id,
|
||||
heritage,
|
||||
templateParameters,
|
||||
decorators,
|
||||
properties,
|
||||
...finishNode(pos),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: SyntaxKind.ModelStatement,
|
||||
id,
|
||||
heritage,
|
||||
templateParameters,
|
||||
decorators,
|
||||
properties,
|
||||
...finishNode(pos),
|
||||
};
|
||||
}
|
||||
|
||||
function parseOptionalModelHeritage() {
|
||||
|
@ -480,7 +485,7 @@ export function parse(code: string | SourceFile) {
|
|||
pos: number,
|
||||
decorators: DecoratorExpressionNode[]
|
||||
): ModelPropertyNode | ModelSpreadPropertyNode {
|
||||
let id =
|
||||
const id =
|
||||
token() === Token.StringLiteral
|
||||
? parseStringLiteral()
|
||||
: parseIdentifier("Property expected.");
|
||||
|
@ -499,6 +504,68 @@ export function parse(code: string | SourceFile) {
|
|||
};
|
||||
}
|
||||
|
||||
function parseEnumStatement(decorators: DecoratorExpressionNode[]): EnumStatementNode {
|
||||
const pos = tokenPos();
|
||||
parseExpected(Token.EnumKeyword);
|
||||
const id = parseIdentifier();
|
||||
const members = parseList(ListKind.EnumMembers, parseEnumMember);
|
||||
return {
|
||||
kind: SyntaxKind.EnumStatement,
|
||||
id,
|
||||
decorators,
|
||||
members,
|
||||
...finishNode(pos),
|
||||
};
|
||||
}
|
||||
|
||||
function parseEnumMember(pos: number, decorators: DecoratorExpressionNode[]): EnumMemberNode {
|
||||
const id =
|
||||
token() === Token.StringLiteral
|
||||
? parseStringLiteral()
|
||||
: parseIdentifier("Enum member expected.");
|
||||
|
||||
let value: StringLiteralNode | NumericLiteralNode | undefined;
|
||||
if (parseOptional(Token.Colon)) {
|
||||
const expr = parseExpression();
|
||||
|
||||
if (expr.kind === SyntaxKind.StringLiteral || expr.kind === SyntaxKind.NumericLiteral) {
|
||||
value = expr;
|
||||
} else if (getFlag(expr, NodeFlags.ThisNodeHasError)) {
|
||||
parseErrorInNextFinishedNode = true;
|
||||
} else {
|
||||
error("Expected numeric or string literal", expr);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: SyntaxKind.EnumMember,
|
||||
id,
|
||||
value,
|
||||
decorators,
|
||||
...finishNode(pos),
|
||||
};
|
||||
}
|
||||
|
||||
function parseAliasStatement(): AliasStatementNode {
|
||||
const pos = tokenPos();
|
||||
parseExpected(Token.AliasKeyword);
|
||||
const id = parseIdentifier();
|
||||
const templateParameters = parseOptionalList(
|
||||
ListKind.TemplateParameters,
|
||||
parseTemplateParameter
|
||||
);
|
||||
parseExpected(Token.Equals);
|
||||
const value = parseExpression();
|
||||
parseExpected(Token.Semicolon);
|
||||
return {
|
||||
kind: SyntaxKind.AliasStatement,
|
||||
id,
|
||||
templateParameters,
|
||||
value,
|
||||
...finishNode(pos),
|
||||
};
|
||||
}
|
||||
|
||||
function parseExpression(): Expression {
|
||||
return parseUnionExpressionOrHigher();
|
||||
}
|
||||
|
@ -541,7 +608,7 @@ export function parse(code: string | SourceFile) {
|
|||
}
|
||||
|
||||
return {
|
||||
kind: SyntaxKind.UnionExpression,
|
||||
kind: SyntaxKind.IntersectionExpression,
|
||||
options,
|
||||
...finishNode(pos),
|
||||
};
|
||||
|
@ -1063,9 +1130,20 @@ export function visitChildren<T>(node: Node, cb: NodeCb<T>): T | undefined {
|
|||
visitNode(cb, node.id) ||
|
||||
visitEach(cb, node.templateParameters) ||
|
||||
visitEach(cb, node.heritage) ||
|
||||
visitNode(cb, node.assignment) ||
|
||||
visitEach(cb, node.properties)
|
||||
);
|
||||
case SyntaxKind.EnumStatement:
|
||||
return (
|
||||
visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitEach(cb, node.members)
|
||||
);
|
||||
case SyntaxKind.EnumMember:
|
||||
return visitEach(cb, node.decorators) || visitNode(cb, node.id) || visitNode(cb, node.value);
|
||||
case SyntaxKind.AliasStatement:
|
||||
return (
|
||||
visitNode(cb, node.id) ||
|
||||
visitEach(cb, node.templateParameters) ||
|
||||
visitNode(cb, node.value)
|
||||
);
|
||||
case SyntaxKind.NamedImport:
|
||||
return visitNode(cb, node.id);
|
||||
case SyntaxKind.TypeReference:
|
||||
|
|
|
@ -32,6 +32,8 @@ export interface Program {
|
|||
executeModelDecorators(type: ModelType): void;
|
||||
executeDecorators(type: Type): void;
|
||||
executeDecorator(node: DecoratorExpressionNode, program: Program, type: Type): void;
|
||||
stateSet(key: Symbol): Set<any>;
|
||||
stateMap(key: Symbol): Map<any, any>;
|
||||
}
|
||||
|
||||
export async function createProgram(
|
||||
|
@ -39,6 +41,8 @@ export async function createProgram(
|
|||
options: CompilerOptions
|
||||
): Promise<Program> {
|
||||
const buildCbs: any = [];
|
||||
const stateMaps = new Map<Symbol, Map<any, any>>();
|
||||
const stateSets = new Map<Symbol, Set<any>>();
|
||||
|
||||
const seenSourceFiles = new Set<string>();
|
||||
const program: Program = {
|
||||
|
@ -52,6 +56,8 @@ export async function createProgram(
|
|||
executeDecorators,
|
||||
executeDecorator,
|
||||
getOption,
|
||||
stateMap,
|
||||
stateSet,
|
||||
onBuild(cb) {
|
||||
buildCbs.push(cb);
|
||||
},
|
||||
|
@ -142,7 +148,7 @@ export async function createProgram(
|
|||
* just the raw type objects, but literals are treated specially.
|
||||
*/
|
||||
function toJSON(type: Type): Type | string | number | boolean {
|
||||
if ("value" in type) {
|
||||
if (type.kind === "Number" || type.kind === "String" || type.kind === "Boolean") {
|
||||
return type.value;
|
||||
}
|
||||
|
||||
|
@ -355,6 +361,26 @@ export async function createProgram(
|
|||
function getOption(key: string): string | undefined {
|
||||
return (options.miscOptions || {})[key];
|
||||
}
|
||||
|
||||
function stateMap(key: Symbol): Map<any, any> {
|
||||
let m = stateMaps.get(key);
|
||||
if (!m) {
|
||||
m = new Map();
|
||||
stateMaps.set(key, m);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
function stateSet(key: Symbol): Set<any> {
|
||||
let s = stateSets.get(key);
|
||||
if (!s) {
|
||||
s = new Set();
|
||||
stateSets.set(key, s);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export async function compile(rootDir: string, host: CompilerHost, options?: CompilerOptions) {
|
||||
|
|
|
@ -58,6 +58,7 @@ export enum Token {
|
|||
Question = 25,
|
||||
Colon = 26,
|
||||
At = 27,
|
||||
// Update MaxPunctuation if anything is added right above here
|
||||
|
||||
// Identifiers
|
||||
Identifier = 28,
|
||||
|
@ -68,11 +69,15 @@ export enum Token {
|
|||
NamespaceKeyword = 31,
|
||||
UsingKeyword = 32,
|
||||
OpKeyword = 33,
|
||||
EnumKeyword = 34,
|
||||
AliasKeyword = 35,
|
||||
// Update MaxStatementKeyword if anything is added right above here
|
||||
|
||||
// Other keywords
|
||||
ExtendsKeyword = 34,
|
||||
TrueKeyword = 35,
|
||||
FalseKeyword = 36,
|
||||
ExtendsKeyword = 36,
|
||||
TrueKeyword = 37,
|
||||
FalseKeyword = 38,
|
||||
// Update MaxKeyword if anything is added right above here
|
||||
}
|
||||
|
||||
const MinKeyword = Token.ImportKeyword;
|
||||
|
@ -82,7 +87,7 @@ const MinPunctuation = Token.OpenBrace;
|
|||
const MaxPunctuation = Token.At;
|
||||
|
||||
const MinStatementKeyword = Token.ImportKeyword;
|
||||
const MaxStatementKeyword = Token.OpKeyword;
|
||||
const MaxStatementKeyword = Token.AliasKeyword;
|
||||
|
||||
/** @internal */
|
||||
export const TokenDisplay: readonly string[] = [
|
||||
|
@ -120,6 +125,8 @@ export const TokenDisplay: readonly string[] = [
|
|||
"'namespace'",
|
||||
"'using'",
|
||||
"'op'",
|
||||
"'enum'",
|
||||
"'alias'",
|
||||
"'extends'",
|
||||
"'true'",
|
||||
"'false'",
|
||||
|
@ -133,6 +140,8 @@ export const Keywords: ReadonlyMap<string, Token> = new Map([
|
|||
["using", Token.UsingKeyword],
|
||||
["op", Token.OpKeyword],
|
||||
["extends", Token.ExtendsKeyword],
|
||||
["enum", Token.EnumKeyword],
|
||||
["alias", Token.AliasKeyword],
|
||||
["true", Token.TrueKeyword],
|
||||
["false", Token.FalseKeyword],
|
||||
]);
|
||||
|
@ -141,7 +150,7 @@ export const Keywords: ReadonlyMap<string, Token> = new Map([
|
|||
export const enum KeywordLimit {
|
||||
MinLength = 2,
|
||||
MaxLength = 9,
|
||||
MinStartChar = CharCode.e,
|
||||
MinStartChar = CharCode.a,
|
||||
MaxStartChar = CharCode.u,
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ export interface BaseType {
|
|||
export type Type =
|
||||
| ModelType
|
||||
| ModelTypeProperty
|
||||
| EnumType
|
||||
| EnumMemberType
|
||||
| TemplateParameterType
|
||||
| NamespaceType
|
||||
| OperationType
|
||||
|
@ -35,7 +37,6 @@ export interface ModelType extends BaseType {
|
|||
baseModels: ModelType[];
|
||||
templateArguments?: Type[];
|
||||
templateNode?: Node;
|
||||
assignmentType?: Type;
|
||||
}
|
||||
|
||||
export interface ModelTypeProperty {
|
||||
|
@ -49,6 +50,22 @@ export interface ModelTypeProperty {
|
|||
optional: boolean;
|
||||
}
|
||||
|
||||
export interface EnumType extends BaseType {
|
||||
kind: "Enum";
|
||||
name: string;
|
||||
node: EnumStatementNode;
|
||||
namespace?: NamespaceType;
|
||||
members: EnumMemberType[];
|
||||
}
|
||||
|
||||
export interface EnumMemberType extends BaseType {
|
||||
kind: "EnumMember";
|
||||
name: string;
|
||||
enum: EnumType;
|
||||
node: EnumMemberNode;
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
export interface OperationType {
|
||||
kind: "Operation";
|
||||
node: OperationStatementNode;
|
||||
|
@ -166,6 +183,9 @@ export enum SyntaxKind {
|
|||
ModelExpression,
|
||||
ModelProperty,
|
||||
ModelSpreadProperty,
|
||||
EnumStatement,
|
||||
EnumMember,
|
||||
AliasStatement,
|
||||
UnionExpression,
|
||||
IntersectionExpression,
|
||||
TupleExpression,
|
||||
|
@ -190,7 +210,7 @@ export type Node =
|
|||
| ModelPropertyNode
|
||||
| OperationStatementNode
|
||||
| NamedImportNode
|
||||
| ModelPropertyNode
|
||||
| EnumMemberNode
|
||||
| ModelSpreadPropertyNode
|
||||
| DecoratorExpressionNode
|
||||
| Statement
|
||||
|
@ -214,6 +234,8 @@ export type Statement =
|
|||
| ModelStatementNode
|
||||
| NamespaceStatementNode
|
||||
| UsingStatementNode
|
||||
| EnumStatementNode
|
||||
| AliasStatementNode
|
||||
| OperationStatementNode
|
||||
| EmptyStatementNode
|
||||
| InvalidStatementNode;
|
||||
|
@ -227,9 +249,15 @@ export type Declaration =
|
|||
| ModelStatementNode
|
||||
| NamespaceStatementNode
|
||||
| OperationStatementNode
|
||||
| TemplateParameterDeclarationNode;
|
||||
| TemplateParameterDeclarationNode
|
||||
| EnumStatementNode
|
||||
| AliasStatementNode;
|
||||
|
||||
export type ScopeNode = NamespaceStatementNode | ModelStatementNode | ADLScriptNode;
|
||||
export type ScopeNode =
|
||||
| NamespaceStatementNode
|
||||
| ModelStatementNode
|
||||
| AliasStatementNode
|
||||
| ADLScriptNode;
|
||||
|
||||
export interface ImportStatementNode extends BaseNode {
|
||||
kind: SyntaxKind.ImportStatement;
|
||||
|
@ -300,12 +328,33 @@ export interface ModelStatementNode extends BaseNode, DeclarationNode {
|
|||
id: IdentifierNode;
|
||||
properties?: (ModelPropertyNode | ModelSpreadPropertyNode)[];
|
||||
heritage: ReferenceExpression[];
|
||||
assignment?: Expression;
|
||||
templateParameters: TemplateParameterDeclarationNode[];
|
||||
locals?: SymbolTable;
|
||||
decorators: DecoratorExpressionNode[];
|
||||
}
|
||||
|
||||
export interface EnumStatementNode extends BaseNode, DeclarationNode {
|
||||
kind: SyntaxKind.EnumStatement;
|
||||
id: IdentifierNode;
|
||||
members: EnumMemberNode[];
|
||||
decorators: DecoratorExpressionNode[];
|
||||
}
|
||||
|
||||
export interface EnumMemberNode extends BaseNode {
|
||||
kind: SyntaxKind.EnumMember;
|
||||
id: IdentifierNode | StringLiteralNode;
|
||||
value?: StringLiteralNode | NumericLiteralNode;
|
||||
decorators: DecoratorExpressionNode[];
|
||||
}
|
||||
|
||||
export interface AliasStatementNode extends BaseNode, DeclarationNode {
|
||||
kind: SyntaxKind.AliasStatement;
|
||||
id: IdentifierNode;
|
||||
value: Expression;
|
||||
templateParameters: TemplateParameterDeclarationNode[];
|
||||
locals?: SymbolTable;
|
||||
}
|
||||
|
||||
export interface InvalidStatementNode extends BaseNode {
|
||||
kind: SyntaxKind.InvalidStatement;
|
||||
}
|
||||
|
|
|
@ -2,14 +2,13 @@ import { throwDiagnostic } from "../compiler/diagnostics.js";
|
|||
import { Program } from "../compiler/program.js";
|
||||
import { ModelTypeProperty, NamespaceType, Type } from "../compiler/types.js";
|
||||
|
||||
const docs = new Map<Type, string>();
|
||||
|
||||
const docsKey = Symbol();
|
||||
export function doc(program: Program, target: Type, text: string) {
|
||||
docs.set(target, text);
|
||||
program.stateMap(docsKey).set(target, text);
|
||||
}
|
||||
|
||||
export function getDoc(target: Type) {
|
||||
return docs.get(target);
|
||||
export function getDoc(program: Program, target: Type): string {
|
||||
return program.stateMap(docsKey).get(target);
|
||||
}
|
||||
|
||||
export function inspectType(program: Program, target: Type, text: string) {
|
||||
|
@ -22,26 +21,30 @@ export function inspectTypeName(program: Program, target: Type, text: string) {
|
|||
console.log(program.checker!.getTypeName(target));
|
||||
}
|
||||
|
||||
const intrinsics = new Set<Type>();
|
||||
const intrinsicsKey = Symbol();
|
||||
export function intrinsic(program: Program, target: Type) {
|
||||
intrinsics.add(target);
|
||||
program.stateSet(intrinsicsKey).add(target);
|
||||
}
|
||||
|
||||
export function isIntrinsic(target: Type) {
|
||||
return intrinsics.has(target);
|
||||
export function isIntrinsic(program: Program, target: Type) {
|
||||
return program.stateSet(intrinsicsKey).has(target);
|
||||
}
|
||||
|
||||
// Walks the assignmentType chain to find the core intrinsic type, if any
|
||||
export function getIntrinsicType(target: Type | undefined): string | undefined {
|
||||
export function getIntrinsicType(program: Program, target: Type | undefined): string | undefined {
|
||||
while (target) {
|
||||
if (target.kind === "Model") {
|
||||
if (isIntrinsic(target)) {
|
||||
if (isIntrinsic(program, target)) {
|
||||
return target.name;
|
||||
}
|
||||
|
||||
target = (target.assignmentType?.kind === "Model" && target.assignmentType) || undefined;
|
||||
if (target.baseModels.length === 1) {
|
||||
target = target.baseModels[0];
|
||||
} else {
|
||||
target = undefined;
|
||||
}
|
||||
} else if (target.kind === "ModelProperty") {
|
||||
return getIntrinsicType(target.type);
|
||||
return getIntrinsicType(program, target.type);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -50,33 +53,32 @@ export function getIntrinsicType(target: Type | undefined): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const numericTypes = new Set<string>();
|
||||
|
||||
const numericTypesKey = Symbol();
|
||||
export function numeric(program: Program, target: Type) {
|
||||
if (!isIntrinsic(target)) {
|
||||
if (!isIntrinsic(program, target)) {
|
||||
throwDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target);
|
||||
}
|
||||
if (target.kind === "Model") {
|
||||
numericTypes.add(target.name);
|
||||
program.stateSet(numericTypesKey).add(target.name);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @numeric decorator to non-model type.", target);
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumericType(target: Type): boolean {
|
||||
const intrinsicType = getIntrinsicType(target);
|
||||
return intrinsicType !== undefined && numericTypes.has(intrinsicType);
|
||||
export function isNumericType(program: Program, target: Type): boolean {
|
||||
const intrinsicType = getIntrinsicType(program, target);
|
||||
return intrinsicType !== undefined && program.stateSet(numericTypesKey).has(intrinsicType);
|
||||
}
|
||||
|
||||
// -- @format decorator ---------------------
|
||||
|
||||
const formatValues = new Map<Type, string>();
|
||||
const formatValuesKey = Symbol();
|
||||
|
||||
export function format(program: Program, target: Type, format: string) {
|
||||
if (target.kind === "Model" || target.kind === "ModelProperty") {
|
||||
// Is it a model type that ultimately derives from 'string'?
|
||||
if (getIntrinsicType(target) === "string") {
|
||||
formatValues.set(target, format);
|
||||
if (getIntrinsicType(program, target) === "string") {
|
||||
program.stateMap(formatValuesKey).set(target, format);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @format to a non-string type", target);
|
||||
}
|
||||
|
@ -85,19 +87,19 @@ export function format(program: Program, target: Type, format: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getFormat(target: Type): string | undefined {
|
||||
return formatValues.get(target);
|
||||
export function getFormat(program: Program, target: Type): string | undefined {
|
||||
return program.stateMap(formatValuesKey).get(target);
|
||||
}
|
||||
|
||||
// -- @minLength decorator ---------------------
|
||||
|
||||
const minLengthValues = new Map<Type, number>();
|
||||
const minLengthValuesKey = Symbol();
|
||||
|
||||
export function minLength(program: Program, target: Type, minLength: number) {
|
||||
if (target.kind === "Model" || target.kind === "ModelProperty") {
|
||||
// Is it a model type that ultimately derives from 'string'?
|
||||
if (getIntrinsicType(target) === "string") {
|
||||
minLengthValues.set(target, minLength);
|
||||
if (getIntrinsicType(program, target) === "string") {
|
||||
program.stateMap(minLengthValuesKey).set(target, minLength);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @minLength to a non-string type", target);
|
||||
}
|
||||
|
@ -109,19 +111,19 @@ export function minLength(program: Program, target: Type, minLength: number) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getMinLength(target: Type): number | undefined {
|
||||
return minLengthValues.get(target);
|
||||
export function getMinLength(program: Program, target: Type): number | undefined {
|
||||
return program.stateMap(minLengthValuesKey).get(target);
|
||||
}
|
||||
|
||||
// -- @maxLength decorator ---------------------
|
||||
|
||||
const maxLengthValues = new Map<Type, number>();
|
||||
const maxLengthValuesKey = Symbol();
|
||||
|
||||
export function maxLength(program: Program, target: Type, maxLength: number) {
|
||||
if (target.kind === "Model" || target.kind === "ModelProperty") {
|
||||
// Is it a model type that ultimately derives from 'string'?
|
||||
if (getIntrinsicType(target) === "string") {
|
||||
maxLengthValues.set(target, maxLength);
|
||||
if (getIntrinsicType(program, target) === "string") {
|
||||
program.stateMap(maxLengthValuesKey).set(target, maxLength);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @maxLength to a non-string type", target);
|
||||
}
|
||||
|
@ -133,19 +135,19 @@ export function maxLength(program: Program, target: Type, maxLength: number) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getMaxLength(target: Type): number | undefined {
|
||||
return maxLengthValues.get(target);
|
||||
export function getMaxLength(program: Program, target: Type): number | undefined {
|
||||
return program.stateMap(maxLengthValuesKey).get(target);
|
||||
}
|
||||
|
||||
// -- @minValue decorator ---------------------
|
||||
|
||||
const minValues = new Map<Type, number>();
|
||||
const minValuesKey = Symbol();
|
||||
|
||||
export function minValue(program: Program, target: Type, minValue: number) {
|
||||
if (target.kind === "Model" || target.kind === "ModelProperty") {
|
||||
// Is it ultimately a numeric type?
|
||||
if (isNumericType(target)) {
|
||||
minValues.set(target, minValue);
|
||||
if (isNumericType(program, target)) {
|
||||
program.stateMap(minValuesKey).set(target, minValue);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @minValue to a non-numeric type", target);
|
||||
}
|
||||
|
@ -157,19 +159,19 @@ export function minValue(program: Program, target: Type, minValue: number) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getMinValue(target: Type): number | undefined {
|
||||
return minValues.get(target);
|
||||
export function getMinValue(program: Program, target: Type): number | undefined {
|
||||
return program.stateMap(minValuesKey).get(target);
|
||||
}
|
||||
|
||||
// -- @maxValue decorator ---------------------
|
||||
|
||||
const maxValues = new Map<Type, number>();
|
||||
const maxValuesKey = Symbol();
|
||||
|
||||
export function maxValue(program: Program, target: Type, maxValue: number) {
|
||||
if (target.kind === "Model" || target.kind === "ModelProperty") {
|
||||
// Is it ultimately a numeric type?
|
||||
if (isNumericType(target)) {
|
||||
maxValues.set(target, maxValue);
|
||||
if (isNumericType(program, target)) {
|
||||
program.stateMap(maxValuesKey).set(target, maxValue);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @maxValue to a non-numeric type", target);
|
||||
}
|
||||
|
@ -181,19 +183,19 @@ export function maxValue(program: Program, target: Type, maxValue: number) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getMaxValue(target: Type): number | undefined {
|
||||
return maxValues.get(target);
|
||||
export function getMaxValue(program: Program, target: Type): number | undefined {
|
||||
return program.stateMap(maxValuesKey).get(target);
|
||||
}
|
||||
|
||||
// -- @secret decorator ---------------------
|
||||
|
||||
const secretTypes = new Map<Type, boolean>();
|
||||
const secretTypesKey = Symbol();
|
||||
|
||||
export function secret(program: Program, target: Type) {
|
||||
if (target.kind === "Model") {
|
||||
// Is it a model type that ultimately derives from 'string'?
|
||||
if (getIntrinsicType(target) === "string") {
|
||||
secretTypes.set(target, true);
|
||||
if (getIntrinsicType(program, target) === "string") {
|
||||
program.stateMap(secretTypesKey).set(target, true);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @secret to a non-string type", target);
|
||||
}
|
||||
|
@ -202,24 +204,24 @@ export function secret(program: Program, target: Type) {
|
|||
}
|
||||
}
|
||||
|
||||
export function isSecret(target: Type): boolean | undefined {
|
||||
return secretTypes.get(target);
|
||||
export function isSecret(program: Program, target: Type): boolean | undefined {
|
||||
return program.stateMap(secretTypesKey).get(target);
|
||||
}
|
||||
|
||||
// -- @visibility decorator ---------------------
|
||||
|
||||
const visibilitySettings = new Map<Type, string[]>();
|
||||
const visibilitySettingsKey = Symbol();
|
||||
|
||||
export function visibility(program: Program, target: Type, ...visibilities: string[]) {
|
||||
if (target.kind === "ModelProperty") {
|
||||
visibilitySettings.set(target, visibilities);
|
||||
program.stateMap(visibilitySettingsKey).set(target, visibilities);
|
||||
} else {
|
||||
throwDiagnostic("The @visibility decorator can only be applied to model properties.", target);
|
||||
}
|
||||
}
|
||||
|
||||
export function getVisibility(target: Type): string[] | undefined {
|
||||
return visibilitySettings.get(target);
|
||||
export function getVisibility(program: Program, target: Type): string[] | undefined {
|
||||
return program.stateMap(visibilitySettingsKey).get(target);
|
||||
}
|
||||
|
||||
export function withVisibility(program: Program, target: Type, ...visibilities: string[]) {
|
||||
|
@ -228,7 +230,7 @@ export function withVisibility(program: Program, target: Type, ...visibilities:
|
|||
}
|
||||
|
||||
const filter = (_: any, prop: ModelTypeProperty) => {
|
||||
const vis = getVisibility(prop);
|
||||
const vis = getVisibility(program, prop);
|
||||
return vis !== undefined && visibilities.filter((v) => !vis.includes(v)).length > 0;
|
||||
};
|
||||
|
||||
|
@ -248,11 +250,11 @@ function mapFilterOut(
|
|||
|
||||
// -- @list decorator ---------------------
|
||||
|
||||
const listProperties = new Set<Type>();
|
||||
const listPropertiesKey = Symbol();
|
||||
|
||||
export function list(program: Program, target: Type) {
|
||||
if (target.kind === "Operation" || target.kind === "ModelProperty") {
|
||||
listProperties.add(target);
|
||||
program.stateSet(listPropertiesKey).add(target);
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
"The @list decorator can only be applied to interface or model properties.",
|
||||
|
@ -261,22 +263,22 @@ export function list(program: Program, target: Type) {
|
|||
}
|
||||
}
|
||||
|
||||
export function isList(target: Type): boolean {
|
||||
return listProperties.has(target);
|
||||
export function isList(program: Program, target: Type): boolean {
|
||||
return program.stateSet(listPropertiesKey).has(target);
|
||||
}
|
||||
|
||||
// -- @tag decorator ---------------------
|
||||
const tagProperties = new Map<Type, string[]>();
|
||||
const tagPropertiesKey = Symbol();
|
||||
|
||||
// Set a tag on an operation or namespace. There can be multiple tags on either an
|
||||
// operation or namespace.
|
||||
export function tag(program: Program, target: Type, tag: string) {
|
||||
if (target.kind === "Operation" || target.kind === "Namespace") {
|
||||
const tags = tagProperties.get(target);
|
||||
const tags = program.stateMap(tagPropertiesKey).get(target);
|
||||
if (tags) {
|
||||
tags.push(tag);
|
||||
} else {
|
||||
tagProperties.set(target, [tag]);
|
||||
program.stateMap(tagPropertiesKey).set(target, [tag]);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic("The @tag decorator can only be applied to namespace or operation.", target);
|
||||
|
@ -284,20 +286,24 @@ export function tag(program: Program, target: Type, tag: string) {
|
|||
}
|
||||
|
||||
// Return the tags set on an operation or namespace
|
||||
export function getTags(target: Type): string[] {
|
||||
return tagProperties.get(target) || [];
|
||||
export function getTags(program: Program, target: Type): string[] {
|
||||
return program.stateMap(tagPropertiesKey).get(target) || [];
|
||||
}
|
||||
|
||||
// Merge the tags for a operation with the tags that are on the namespace it resides within.
|
||||
//
|
||||
// TODO: (JC) We'll need to update this for nested namespaces
|
||||
export function getAllTags(namespace: NamespaceType, target: Type): string[] | undefined {
|
||||
export function getAllTags(
|
||||
program: Program,
|
||||
namespace: NamespaceType,
|
||||
target: Type
|
||||
): string[] | undefined {
|
||||
const tags = new Set<string>();
|
||||
|
||||
for (const t of getTags(namespace)) {
|
||||
for (const t of getTags(program, namespace)) {
|
||||
tags.add(t);
|
||||
}
|
||||
for (const t of getTags(target)) {
|
||||
for (const t of getTags(program, target)) {
|
||||
tags.add(t);
|
||||
}
|
||||
return tags.size > 0 ? Array.from(tags) : undefined;
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
import { ok, strictEqual } from "assert";
|
||||
import { ModelType, UnionType } from "../../compiler/types.js";
|
||||
import { createTestHost, TestHost } from "../test-host.js";
|
||||
|
||||
describe("aliases", () => {
|
||||
let testHost: TestHost;
|
||||
|
||||
beforeEach(async () => {
|
||||
testHost = await createTestHost();
|
||||
});
|
||||
|
||||
it("can alias a union expression", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
alias Foo = int32 | string;
|
||||
alias Bar = "hi" | 10;
|
||||
alias FooBar = Foo | Bar;
|
||||
|
||||
@test model A {
|
||||
prop: FooBar
|
||||
}
|
||||
`
|
||||
);
|
||||
const { A } = (await testHost.compile("./")) as {
|
||||
A: ModelType;
|
||||
};
|
||||
|
||||
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
|
||||
strictEqual(propType.kind, "Union");
|
||||
strictEqual(propType.options.length, 4);
|
||||
strictEqual(propType.options[0].kind, "Model");
|
||||
strictEqual(propType.options[1].kind, "Model");
|
||||
strictEqual(propType.options[2].kind, "String");
|
||||
strictEqual(propType.options[3].kind, "Number");
|
||||
});
|
||||
|
||||
it("can alias a deep union expression", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
alias Foo = int32 | string;
|
||||
alias Bar = "hi" | 10;
|
||||
alias Baz = Foo | Bar;
|
||||
alias FooBar = Baz | "bye";
|
||||
|
||||
@test model A {
|
||||
prop: FooBar
|
||||
}
|
||||
`
|
||||
);
|
||||
const { A } = (await testHost.compile("./")) as {
|
||||
A: ModelType;
|
||||
};
|
||||
|
||||
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
|
||||
strictEqual(propType.kind, "Union");
|
||||
strictEqual(propType.options.length, 5);
|
||||
strictEqual(propType.options[0].kind, "Model");
|
||||
strictEqual(propType.options[1].kind, "Model");
|
||||
strictEqual(propType.options[2].kind, "String");
|
||||
strictEqual(propType.options[3].kind, "Number");
|
||||
strictEqual(propType.options[4].kind, "String");
|
||||
});
|
||||
|
||||
it("can alias a union expression with parameters", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
alias Foo<T> = int32 | T;
|
||||
|
||||
@test model A {
|
||||
prop: Foo<"hi">
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { A } = (await testHost.compile("./")) as {
|
||||
A: ModelType;
|
||||
};
|
||||
|
||||
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
|
||||
strictEqual(propType.kind, "Union");
|
||||
strictEqual(propType.options.length, 2);
|
||||
strictEqual(propType.options[0].kind, "Model");
|
||||
strictEqual(propType.options[1].kind, "String");
|
||||
});
|
||||
|
||||
it("can alias a deep union expression with parameters", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
alias Foo<T> = int32 | T;
|
||||
alias Bar<T, U> = Foo<T> | Foo<U>;
|
||||
|
||||
@test model A {
|
||||
prop: Bar<"hi", 42>
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { A } = (await testHost.compile("./")) as {
|
||||
A: ModelType;
|
||||
};
|
||||
|
||||
const propType: UnionType = A.properties.get("prop")!.type as UnionType;
|
||||
strictEqual(propType.kind, "Union");
|
||||
strictEqual(propType.options.length, 4);
|
||||
strictEqual(propType.options[0].kind, "Model");
|
||||
strictEqual(propType.options[1].kind, "String");
|
||||
strictEqual(propType.options[2].kind, "Model");
|
||||
strictEqual(propType.options[3].kind, "Number");
|
||||
});
|
||||
|
||||
it("can alias an intersection expression", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
alias Foo = {a: string} & {b: string};
|
||||
alias Bar = {c: string} & {d: string};
|
||||
alias FooBar = Foo & Bar;
|
||||
|
||||
@test model A {
|
||||
prop: FooBar
|
||||
}
|
||||
`
|
||||
);
|
||||
const { A } = (await testHost.compile("./")) as {
|
||||
A: ModelType;
|
||||
};
|
||||
|
||||
const propType: ModelType = A.properties.get("prop")!.type as ModelType;
|
||||
strictEqual(propType.kind, "Model");
|
||||
strictEqual(propType.properties.size, 4);
|
||||
ok(propType.properties.has("a"));
|
||||
ok(propType.properties.has("b"));
|
||||
ok(propType.properties.has("c"));
|
||||
ok(propType.properties.has("d"));
|
||||
});
|
||||
|
||||
it("can be used like any model", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
@test model Test { a: string };
|
||||
|
||||
alias Alias = Test;
|
||||
|
||||
@test model A extends Alias { };
|
||||
@test model B { ... Alias };
|
||||
@test model C { c: Alias };
|
||||
`
|
||||
);
|
||||
const { Test, A, B, C } = (await testHost.compile("./")) as {
|
||||
Test: ModelType;
|
||||
A: ModelType;
|
||||
B: ModelType;
|
||||
C: ModelType;
|
||||
};
|
||||
|
||||
strictEqual(A.baseModels[0], Test);
|
||||
ok(B.properties.has("a"));
|
||||
strictEqual(C.properties.get("c")!.type, Test);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
import { ok, strictEqual } from "assert";
|
||||
import { EnumMemberType, EnumType, ModelType } from "../../compiler/types.js";
|
||||
import { createTestHost, TestHost } from "../test-host.js";
|
||||
|
||||
describe("enums", () => {
|
||||
let testHost: TestHost;
|
||||
|
||||
beforeEach(async () => {
|
||||
testHost = await createTestHost();
|
||||
});
|
||||
|
||||
it("can be valueless", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
@test enum E {
|
||||
A, B, C
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { E } = (await testHost.compile("./")) as {
|
||||
E: EnumType;
|
||||
};
|
||||
|
||||
ok(E);
|
||||
ok(!E.members[0].value);
|
||||
ok(!E.members[1].value);
|
||||
ok(!E.members[2].value);
|
||||
});
|
||||
|
||||
it("can have values", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
@test enum E {
|
||||
@test("A") A: "a";
|
||||
@test("B") B: "b";
|
||||
@test("C") C: "c";
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { E, A, B, C } = (await testHost.compile("./")) as {
|
||||
E: EnumType;
|
||||
A: EnumMemberType;
|
||||
B: EnumMemberType;
|
||||
C: EnumMemberType;
|
||||
};
|
||||
|
||||
ok(E);
|
||||
strictEqual(A.value, "a");
|
||||
strictEqual(B.value, "b");
|
||||
strictEqual(C.value, "c");
|
||||
});
|
||||
|
||||
it("can be a model property", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
namespace Foo;
|
||||
enum E { A, B, C }
|
||||
@test model Foo {
|
||||
prop: E;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const { Foo } = (await testHost.compile("./")) as {
|
||||
Foo: ModelType;
|
||||
};
|
||||
|
||||
ok(Foo);
|
||||
strictEqual(Foo.properties.get("prop")!.type.kind, "Enum");
|
||||
});
|
||||
});
|
|
@ -326,7 +326,7 @@ describe("blockless namespaces", () => {
|
|||
"a.adl",
|
||||
`
|
||||
import "./b.adl";
|
||||
model M = N.X;
|
||||
model M {x: N.X }
|
||||
`
|
||||
);
|
||||
testHost.addAdlFile(
|
||||
|
|
|
@ -167,7 +167,7 @@ describe("using statements", () => {
|
|||
}
|
||||
|
||||
namespace M {
|
||||
model X = A;
|
||||
model X { a: A };
|
||||
}
|
||||
`
|
||||
);
|
||||
|
|
|
@ -21,9 +21,9 @@ describe("range limiting decorators", () => {
|
|||
|
||||
const { A, B } = (await testHost.compile("./")) as { A: ModelType; B: ModelType };
|
||||
|
||||
strictEqual(getMinValue(A.properties.get("foo")!), 15);
|
||||
strictEqual(getMaxValue(A.properties.get("boo")!), 55);
|
||||
strictEqual(getMaxValue(B.properties.get("bar")!), 20);
|
||||
strictEqual(getMinValue(B.properties.get("car")!), 23);
|
||||
strictEqual(getMinValue(testHost.program, A.properties.get("foo")!), 15);
|
||||
strictEqual(getMaxValue(testHost.program, A.properties.get("boo")!), 55);
|
||||
strictEqual(getMaxValue(testHost.program, B.properties.get("bar")!), 20);
|
||||
strictEqual(getMinValue(testHost.program, B.properties.get("car")!), 23);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,5 +2,5 @@ import "MyLib";
|
|||
import "CustomAdlMain";
|
||||
|
||||
@myLibDec
|
||||
model A = MyLib.Model;
|
||||
model B = CustomAdlMain.Model;
|
||||
model A { x: MyLib.Model };
|
||||
model B { x: CustomAdlMain.Model };
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { readdir, readFile } from "fs/promises";
|
||||
import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
||||
import { fileURLToPath, pathToFileURL } from "url";
|
||||
import { CompilerOptions } from "../compiler/options";
|
||||
import { Program } from "../compiler/program";
|
||||
import { createProgram } from "../compiler/program.js";
|
||||
import { CompilerHost, Type } from "../compiler/types";
|
||||
|
||||
export interface TestHost {
|
||||
addAdlFile(path: string, contents: string): void;
|
||||
addJsFile(path: string, contents: any): void;
|
||||
compile(main: string): Promise<Record<string, Type>>;
|
||||
addRealAdlFile(path: string, realPath: string): Promise<void>;
|
||||
addRealJsFile(path: string, realPath: string): Promise<void>;
|
||||
compile(main: string, options?: CompilerOptions): Promise<Record<string, Type>>;
|
||||
testTypes: Record<string, Type>;
|
||||
program: Program;
|
||||
/**
|
||||
* Virtual filesystem used in the tests.
|
||||
*/
|
||||
|
@ -17,7 +22,7 @@ export interface TestHost {
|
|||
|
||||
export async function createTestHost(): Promise<TestHost> {
|
||||
const testTypes: Record<string, Type> = {};
|
||||
|
||||
let program: Program = undefined as any; // in practice it will always be initialized
|
||||
const virtualFs: { [name: string]: string } = {};
|
||||
const jsImports: { [path: string]: Promise<any> } = {};
|
||||
const compilerHost: CompilerHost = {
|
||||
|
@ -66,6 +71,17 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
},
|
||||
|
||||
async stat(path: string) {
|
||||
if (virtualFs.hasOwnProperty(path)) {
|
||||
return {
|
||||
isDirectory() {
|
||||
return false;
|
||||
},
|
||||
isFile() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for (const fsPath of Object.keys(virtualFs)) {
|
||||
if (fsPath.startsWith(path) && fsPath !== path) {
|
||||
return {
|
||||
|
@ -79,14 +95,7 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDirectory() {
|
||||
return false;
|
||||
},
|
||||
isFile() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
throw { code: "ENOENT" };
|
||||
},
|
||||
|
||||
// symlinks not supported in test-host
|
||||
|
@ -120,7 +129,7 @@ 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") {
|
||||
if (target.kind === "Model" || target.kind === "Namespace" || target.kind === "Enum") {
|
||||
name = target.name;
|
||||
} else {
|
||||
throw new Error("Need to specify a name for test type");
|
||||
|
@ -134,8 +143,13 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
return {
|
||||
addAdlFile,
|
||||
addJsFile,
|
||||
addRealAdlFile,
|
||||
addRealJsFile,
|
||||
compile,
|
||||
testTypes,
|
||||
get program() {
|
||||
return program;
|
||||
},
|
||||
fs: virtualFs,
|
||||
};
|
||||
|
||||
|
@ -150,15 +164,35 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
jsImports[key] = new Promise((r) => r(contents));
|
||||
}
|
||||
|
||||
async function compile(main: string) {
|
||||
async function addRealAdlFile(path: string, existingPath: string) {
|
||||
virtualFs[resolve(compilerHost.getCwd(), path)] = await readFile(existingPath, "utf8");
|
||||
}
|
||||
|
||||
async function addRealJsFile(path: string, existingPath: string) {
|
||||
const key = resolve(compilerHost.getCwd(), path);
|
||||
const exports = await import(pathToFileURL(existingPath).href);
|
||||
|
||||
virtualFs[key] = "";
|
||||
jsImports[key] = exports;
|
||||
}
|
||||
|
||||
async function compile(main: string, options: CompilerOptions = {}) {
|
||||
// default is noEmit
|
||||
if (!options.hasOwnProperty("noEmit")) {
|
||||
options.noEmit = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const program = await createProgram(compilerHost, {
|
||||
program = await createProgram(compilerHost, {
|
||||
mainFile: main,
|
||||
noEmit: true,
|
||||
...options,
|
||||
});
|
||||
|
||||
return testTypes;
|
||||
} catch (e) {
|
||||
if (e.diagnostics) {
|
||||
throw e.diagnostics;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,11 @@ describe("syntax", () => {
|
|||
});
|
||||
|
||||
describe("model = statements", () => {
|
||||
parseEach(["model x = y;", "model foo = bar | baz;", "model bar<a, b> = a | b;"]);
|
||||
parseErrorEach([
|
||||
["model x = y;", [/'{' expected/]],
|
||||
["model foo = bar | baz;", [/'{' expected/]],
|
||||
["model bar<a, b> = a | b;", [/'{' expected/]],
|
||||
]);
|
||||
});
|
||||
|
||||
describe("model expressions", () => {
|
||||
|
@ -121,7 +125,7 @@ describe("syntax", () => {
|
|||
});
|
||||
|
||||
describe("template instantiations", () => {
|
||||
parseEach(["model A = Foo<number, string>;", "model B = Foo<number, string>[];"]);
|
||||
parseEach(["model A { x: Foo<number, string>; }", "model B { x: Foo<number, string>[]; }"]);
|
||||
});
|
||||
|
||||
describe("intersection expressions", () => {
|
||||
|
@ -129,7 +133,7 @@ describe("syntax", () => {
|
|||
});
|
||||
|
||||
describe("parenthesized expressions", () => {
|
||||
parseEach(["model A = ((B | C) & D)[];"]);
|
||||
parseEach(["model A { x: ((B | C) & D)[]; }"]);
|
||||
});
|
||||
|
||||
describe("namespace statements", () => {
|
||||
|
@ -165,7 +169,6 @@ describe("syntax", () => {
|
|||
`
|
||||
model A { };
|
||||
model B { }
|
||||
model C = A;
|
||||
;
|
||||
namespace I {
|
||||
op foo(): number;
|
||||
|
@ -221,25 +224,25 @@ describe("syntax", () => {
|
|||
|
||||
describe("unterminated tokens", () => {
|
||||
parseErrorEach([
|
||||
['model X = "banana', [/Unterminated string literal/]],
|
||||
['model X = "banana\\', [/Unterminated string literal/]],
|
||||
['model X = """\nbanana', [/Unterminated string literal/]],
|
||||
['model X = """\nbanana\\', [/Unterminated string literal/]],
|
||||
['alias X = "banana', [/Unterminated string literal/]],
|
||||
['alias X = "banana\\', [/Unterminated string literal/]],
|
||||
['alias X = """\nbanana', [/Unterminated string literal/]],
|
||||
['alias X = """\nbanana\\', [/Unterminated string literal/]],
|
||||
["/* Yada yada yada", [/Unterminated comment/]],
|
||||
]);
|
||||
});
|
||||
|
||||
describe("terminated tokens at EOF with missing semicolon", () => {
|
||||
parseErrorEach([
|
||||
["model X = 0x10101", [/';' expected/]],
|
||||
["model X = 0xBEEF", [/';' expected/]],
|
||||
["model X = 123", [/';' expected/]],
|
||||
["model X = 123e45", [/';' expected/]],
|
||||
["model X = 123.45", [/';' expected/]],
|
||||
["model X = 123.45e2", [/';' expected/]],
|
||||
["model X = Banana", [/';' expected/]],
|
||||
['model X = "Banana"', [/';' expected/]],
|
||||
['model X = """\nBanana\n"""', [/';' expected/]],
|
||||
["alias X = 0x10101", [/';' expected/]],
|
||||
["alias X = 0xBEEF", [/';' expected/]],
|
||||
["alias X = 123", [/';' expected/]],
|
||||
["alias X = 123e45", [/';' expected/]],
|
||||
["alias X = 123.45", [/';' expected/]],
|
||||
["alias X = 123.45e2", [/';' expected/]],
|
||||
["alias X = Banana", [/';' expected/]],
|
||||
['alias X = "Banana"', [/';' expected/]],
|
||||
['alias X = """\nBanana\n"""', [/';' expected/]],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -290,13 +293,13 @@ describe("syntax", () => {
|
|||
["0xG", /Hexadecimal digit expected/],
|
||||
];
|
||||
|
||||
parseEach(good.map((c) => [`model M = ${c[0]};`, (node) => isNumericLiteral(node, c[1])]));
|
||||
parseErrorEach(bad.map((c) => [`model M = ${c[0]};`, [c[1]]]));
|
||||
parseEach(good.map((c) => [`alias M = ${c[0]};`, (node) => isNumericLiteral(node, c[1])]));
|
||||
parseErrorEach(bad.map((c) => [`alias M = ${c[0]};`, [c[1]]]));
|
||||
|
||||
function isNumericLiteral(node: ADLScriptNode, value: number) {
|
||||
const statement = node.statements[0];
|
||||
assert(statement.kind === SyntaxKind.ModelStatement, "model statement expected");
|
||||
const assignment = statement.assignment;
|
||||
assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected");
|
||||
const assignment = statement.value;
|
||||
assert(assignment?.kind === SyntaxKind.NumericLiteral, "numeric literal expected");
|
||||
assert.strictEqual(assignment.value, value);
|
||||
}
|
||||
|
@ -312,6 +315,33 @@ describe("syntax", () => {
|
|||
]);
|
||||
parseErrorEach([["model 😢 {}", [/Invalid character/]]]);
|
||||
});
|
||||
|
||||
describe("enum statements", () => {
|
||||
parseEach([
|
||||
"enum Foo { }",
|
||||
"enum Foo { a, b }",
|
||||
'enum Foo { a: "hi", c: 10 }',
|
||||
"@foo enum Foo { @bar a, @baz b: 10 }",
|
||||
]);
|
||||
|
||||
parseErrorEach([
|
||||
["enum Foo { a: number }", [/Expected numeric or string literal/]],
|
||||
["enum Foo { a: ; b: ; }", [/Expression expected/, /Expression expected/]],
|
||||
["enum Foo { ;+", [/Enum member expected/]],
|
||||
["enum { }", [/Identifier expected/]],
|
||||
]);
|
||||
});
|
||||
|
||||
describe("alias statements", () => {
|
||||
parseEach(["alias X = 1;", "alias X = A | B;", "alias MaybeUndefined<T> = T | undefined;"]);
|
||||
parseErrorEach([
|
||||
["@foo alias Bar = 1;", [/Cannot decorate alias statement/]],
|
||||
["alias Foo =", [/Expression expected/]],
|
||||
["alias Foo<> =", [/Identifier expected/, /Expression expected/]],
|
||||
["alias Foo<T> = X |", [/Expression expected/]],
|
||||
["alias =", [/Identifier expected/]],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function parseEach(cases: (string | [string, (node: ADLScriptNode) => void])[]) {
|
||||
|
@ -359,7 +389,7 @@ function parseErrorEach(cases: [string, RegExp[]][]) {
|
|||
|
||||
logVerboseTestOutput("\n=== Diagnostics ===");
|
||||
logVerboseTestOutput((log) => logDiagnostics(astNode.parseDiagnostics, log));
|
||||
assert.notStrictEqual(astNode.parseDiagnostics.length, 0);
|
||||
assert.notStrictEqual(astNode.parseDiagnostics.length, 0, "parse diagnostics length");
|
||||
let i = 0;
|
||||
for (const match of matches) {
|
||||
assert.match(astNode.parseDiagnostics[i++].message, match);
|
||||
|
|
|
@ -123,6 +123,11 @@ describe("scanner", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("scans intersections", () => {
|
||||
const all = tokens("A&B");
|
||||
verify(all, [[Token.Identifier, "A"], [Token.Ampersand], [Token.Identifier, "B"]]);
|
||||
});
|
||||
|
||||
it("scans decorator expressions", () => {
|
||||
const all = tokens('@foo(1,"hello",foo)');
|
||||
|
||||
|
@ -274,10 +279,26 @@ describe("scanner", () => {
|
|||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(minKeywordLengthFound, KeywordLimit.MinLength);
|
||||
assert.strictEqual(maxKeywordLengthFound, KeywordLimit.MaxLength);
|
||||
assert.strictEqual(minKeywordStartCharFound, KeywordLimit.MinStartChar);
|
||||
assert.strictEqual(maxKeywordStartCharFound, KeywordLimit.MaxStartChar);
|
||||
assert.strictEqual(
|
||||
minKeywordLengthFound,
|
||||
KeywordLimit.MinLength,
|
||||
`min keyword length is incorrect, set KeywordLimit.MinLength to ${minKeywordLengthFound}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
maxKeywordLengthFound,
|
||||
KeywordLimit.MaxLength,
|
||||
`max keyword length is incorrect, set KeywordLimit.MaxLength to ${maxKeywordLengthFound}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
minKeywordStartCharFound,
|
||||
KeywordLimit.MinStartChar,
|
||||
`min keyword start char is incorrect, set KeywordLimit.MinStartChar to ${minKeywordStartCharFound}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
maxKeywordStartCharFound,
|
||||
KeywordLimit.MaxStartChar,
|
||||
`max keyword start char is incorrect, set KeywordLimit.MaxStartChar to ${maxKeywordStartCharFound}`
|
||||
);
|
||||
|
||||
// check single character punctuation
|
||||
for (let i = 33; i <= 126; i++) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче