Implement alias and enum, remove model = (#504)

This commit is contained in:
Brian Terlson 2021-05-03 11:14:24 -07:00 коммит произвёл GitHub
Родитель 051862aa54
Коммит 8c86e04427
19 изменённых файлов: 900 добавлений и 260 удалений

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

@ -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++) {