Add semantic error recovery (#574)
We now continue to semantic analysis when there are parse errors, and we do not abort the compilation when a semantic error is encountered, but instead recover and look for more errors. DiagnosticError is no longer a thing as we never throw diagnostics as exceptions anymore. throwDiagnostic is now Program.reportDiagnostic.
This commit is contained in:
Родитель
451274c269
Коммит
c62c1036f9
|
@ -1,4 +1,4 @@
|
|||
import { NamespaceType, Program, throwDiagnostic, Type } from "@azure-tools/adl";
|
||||
import { NamespaceType, Program, Type } from "@azure-tools/adl";
|
||||
|
||||
const basePathsKey = Symbol();
|
||||
export function resource(program: Program, entity: Type, basePath = "") {
|
||||
|
@ -77,10 +77,10 @@ function setOperationRoute(program: Program, entity: Type, verb: OperationRoute)
|
|||
if (!program.stateMap(operationRoutesKey).has(entity)) {
|
||||
program.stateMap(operationRoutesKey).set(entity, verb);
|
||||
} else {
|
||||
throwDiagnostic(`HTTP verb already applied to ${entity.name}`, entity);
|
||||
program.reportDiagnostic(`HTTP verb already applied to ${entity.name}`, entity);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic(`Cannot use @${verb} on a ${entity.kind}`, entity);
|
||||
program.reportDiagnostic(`Cannot use @${verb} on a ${entity.kind}`, entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,10 @@ function getServiceDetails(program: Program) {
|
|||
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);
|
||||
program.reportDiagnostic(
|
||||
"Cannot set service namespace more than once in an ADL project.",
|
||||
namespace
|
||||
);
|
||||
}
|
||||
|
||||
serviceDetails.namespace = namespace;
|
||||
|
@ -159,11 +162,15 @@ export function _checkIfServiceNamespace(program: Program, namespace: NamespaceT
|
|||
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);
|
||||
program.reportDiagnostic("Service title can only be set once per ADL document.", entity);
|
||||
}
|
||||
|
||||
if (entity.kind !== "Namespace") {
|
||||
throwDiagnostic("The @serviceTitle decorator can only be applied to namespaces.", entity);
|
||||
program.reportDiagnostic(
|
||||
"The @serviceTitle decorator can only be applied to namespaces.",
|
||||
entity
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_setServiceNamespace(program, entity);
|
||||
|
@ -179,11 +186,15 @@ 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);
|
||||
program.reportDiagnostic("Service version can only be set once per ADL document.", entity);
|
||||
}
|
||||
|
||||
if (entity.kind !== "Namespace") {
|
||||
throwDiagnostic("The @serviceVersion decorator can only be applied to namespaces.", entity);
|
||||
program.reportDiagnostic(
|
||||
"The @serviceVersion decorator can only be applied to namespaces.",
|
||||
entity
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_setServiceNamespace(program, entity);
|
||||
|
@ -207,7 +218,7 @@ 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);
|
||||
program.reportDiagnostic("The @produces decorator can only be applied to namespaces.", entity);
|
||||
}
|
||||
|
||||
const values = getProduces(program, entity);
|
||||
|
@ -222,7 +233,7 @@ 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);
|
||||
program.reportDiagnostic("The @consumes decorator can only be applied to namespaces.", entity);
|
||||
}
|
||||
|
||||
const values = getConsumes(program, entity);
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
TemplateParameterDeclarationNode,
|
||||
UsingStatementNode,
|
||||
} from "./types.js";
|
||||
import { reportDuplicateSymbols } from "./util.js";
|
||||
|
||||
const SymbolTable = class extends Map<string, Sym> implements SymbolTable {
|
||||
duplicates = new Set<Sym>();
|
||||
|
@ -56,7 +55,7 @@ export function createSymbolTable(): SymbolTable {
|
|||
return new SymbolTable();
|
||||
}
|
||||
|
||||
export function createBinder(): Binder {
|
||||
export function createBinder(reportDuplicateSymbols: (symbolTable: SymbolTable) => void): Binder {
|
||||
let currentFile: ADLScriptNode;
|
||||
let parentNode: Node;
|
||||
let globalNamespace: NamespaceStatementNode;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { compilerAssert, throwDiagnostic } from "./diagnostics.js";
|
||||
import { compilerAssert } from "./diagnostics.js";
|
||||
import { hasParseError } from "./parser.js";
|
||||
import { Program } from "./program.js";
|
||||
import {
|
||||
ADLScriptNode,
|
||||
|
@ -11,9 +12,9 @@ import {
|
|||
EnumMemberType,
|
||||
EnumStatementNode,
|
||||
EnumType,
|
||||
ErrorType,
|
||||
IdentifierNode,
|
||||
IntersectionExpressionNode,
|
||||
IntrinsicType,
|
||||
LiteralNode,
|
||||
LiteralType,
|
||||
ModelExpressionNode,
|
||||
|
@ -44,7 +45,6 @@ import {
|
|||
UnionExpressionNode,
|
||||
UnionType,
|
||||
} from "./types.js";
|
||||
import { reportDuplicateSymbols } from "./util.js";
|
||||
|
||||
/**
|
||||
* A map keyed by a set of objects.
|
||||
|
@ -97,7 +97,7 @@ export function createChecker(program: Program) {
|
|||
let instantiatingTemplate: Node | undefined;
|
||||
let currentSymbolId = 0;
|
||||
const symbolLinks = new Map<number, SymbolLinks>();
|
||||
const errorType: IntrinsicType = { kind: "Intrinsic", name: "ErrorType" };
|
||||
const errorType: ErrorType = { kind: "Intrinsic", name: "ErrorType" };
|
||||
|
||||
// This variable holds on to the model type that is currently
|
||||
// being instantiated in checkModelStatement so that it is
|
||||
|
@ -108,9 +108,17 @@ export function createChecker(program: Program) {
|
|||
for (const using of file.usings) {
|
||||
const parentNs = using.parent! as NamespaceStatementNode | ADLScriptNode;
|
||||
const sym = resolveTypeReference(using.name);
|
||||
if (sym.kind === "decorator") throwDiagnostic("Can't use a decorator", using);
|
||||
if (!sym) {
|
||||
continue;
|
||||
}
|
||||
if (sym.kind === "decorator") {
|
||||
program.reportDiagnostic("Can't use a decorator", using);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sym.node.kind !== SyntaxKind.NamespaceStatement) {
|
||||
throwDiagnostic("Using must refer to a namespace", using);
|
||||
program.reportDiagnostic("Using must refer to a namespace", using);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [name, binding] of sym.node.exports!) {
|
||||
|
@ -231,12 +239,17 @@ export function createChecker(program: Program) {
|
|||
|
||||
function checkTypeReference(node: TypeReferenceNode): Type {
|
||||
const sym = resolveTypeReference(node);
|
||||
if (!sym) {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
if (sym.kind === "decorator") {
|
||||
throwDiagnostic("Can't put a decorator in a type", node);
|
||||
program.reportDiagnostic("Can't put a decorator in a type", node);
|
||||
return errorType;
|
||||
}
|
||||
|
||||
const symbolLinks = getSymbolLinks(sym);
|
||||
const args = node.arguments.map(getTypeForNode);
|
||||
let args = node.arguments.map(getTypeForNode);
|
||||
|
||||
if (
|
||||
sym.node.kind === SyntaxKind.ModelStatement ||
|
||||
|
@ -245,7 +258,10 @@ export function createChecker(program: Program) {
|
|||
// model statement, possibly templated
|
||||
if (sym.node.templateParameters.length === 0) {
|
||||
if (args.length > 0) {
|
||||
throwDiagnostic("Can't pass template arguments to model that is not templated", node);
|
||||
program.reportDiagnostic(
|
||||
"Can't pass template arguments to model that is not templated",
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
if (symbolLinks.declaredType) {
|
||||
|
@ -267,21 +283,21 @@ export function createChecker(program: Program) {
|
|||
: checkAlias(sym.node);
|
||||
}
|
||||
|
||||
if (sym.node.templateParameters!.length > node.arguments.length) {
|
||||
throwDiagnostic("Too few template arguments provided.", node);
|
||||
const templateParameters = sym.node.templateParameters;
|
||||
if (args.length < templateParameters.length) {
|
||||
program.reportDiagnostic("Too few template arguments provided.", node);
|
||||
args = [...args, ...new Array(templateParameters.length - args.length).fill(errorType)];
|
||||
} else if (args.length > templateParameters.length) {
|
||||
program.reportDiagnostic("Too many template arguments provided.", node);
|
||||
args = args.slice(0, templateParameters.length);
|
||||
}
|
||||
|
||||
if (sym.node.templateParameters!.length < node.arguments.length) {
|
||||
throwDiagnostic("Too many template arguments provided.", node);
|
||||
}
|
||||
|
||||
return instantiateTemplate(sym.node, args);
|
||||
}
|
||||
}
|
||||
// some other kind of reference
|
||||
|
||||
if (args.length > 0) {
|
||||
throwDiagnostic("Can't pass template arguments to non-templated type", node);
|
||||
program.reportDiagnostic("Can't pass template arguments to non-templated type", node);
|
||||
}
|
||||
|
||||
if (sym.node.kind === SyntaxKind.TemplateParameterDeclaration) {
|
||||
|
@ -363,7 +379,8 @@ export function createChecker(program: Program) {
|
|||
function checkIntersectionExpression(node: IntersectionExpressionNode) {
|
||||
const optionTypes = node.options.map(getTypeForNode);
|
||||
if (!allModelTypes(optionTypes)) {
|
||||
throwDiagnostic("Cannot intersect non-model types (including union types).", node);
|
||||
program.reportDiagnostic("Cannot intersect non-model types (including union types).", node);
|
||||
return errorType;
|
||||
}
|
||||
|
||||
const properties = new Map<string, ModelTypeProperty>();
|
||||
|
@ -371,10 +388,11 @@ export function createChecker(program: Program) {
|
|||
const allProps = walkPropertiesInherited(option);
|
||||
for (const prop of allProps) {
|
||||
if (properties.has(prop.name)) {
|
||||
throwDiagnostic(
|
||||
program.reportDiagnostic(
|
||||
`Intersection contains duplicate property definitions for ${prop.name}`,
|
||||
node
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newPropType = createType({
|
||||
|
@ -510,6 +528,12 @@ export function createChecker(program: Program) {
|
|||
}
|
||||
|
||||
function resolveIdentifier(node: IdentifierNode) {
|
||||
if (hasParseError(node)) {
|
||||
// Don't report synthetic identifiers used for parser error recovery.
|
||||
// The parse error is the root cause and will already have been logged.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let scope: Node | undefined = node.parent;
|
||||
let binding;
|
||||
|
||||
|
@ -539,29 +563,38 @@ export function createChecker(program: Program) {
|
|||
if (binding) return binding;
|
||||
}
|
||||
|
||||
throwDiagnostic("Unknown identifier " + node.sv, node);
|
||||
program.reportDiagnostic("Unknown identifier " + node.sv, node);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveTypeReference(node: ReferenceExpression): DecoratorSymbol | TypeSymbol {
|
||||
function resolveTypeReference(
|
||||
node: ReferenceExpression
|
||||
): DecoratorSymbol | TypeSymbol | undefined {
|
||||
if (node.kind === SyntaxKind.TypeReference) {
|
||||
return resolveTypeReference(node.target);
|
||||
}
|
||||
|
||||
if (node.kind === SyntaxKind.MemberExpression) {
|
||||
const base = resolveTypeReference(node.base);
|
||||
if (!base) {
|
||||
return undefined;
|
||||
}
|
||||
if (base.kind === "type" && base.node.kind === SyntaxKind.NamespaceStatement) {
|
||||
const symbol = resolveIdentifierInTable(node.id, base.node.exports!);
|
||||
if (!symbol) {
|
||||
throwDiagnostic(`Namespace doesn't have member ${node.id.sv}`, node);
|
||||
program.reportDiagnostic(`Namespace doesn't have member ${node.id.sv}`, node);
|
||||
return undefined;
|
||||
}
|
||||
return symbol;
|
||||
} else if (base.kind === "decorator") {
|
||||
throwDiagnostic(`Cannot resolve '${node.id.sv}' in decorator`, node);
|
||||
program.reportDiagnostic(`Cannot resolve '${node.id.sv}' in decorator`, node);
|
||||
return undefined;
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
program.reportDiagnostic(
|
||||
`Cannot resolve '${node.id.sv}' in non-namespace node ${base.node.kind}`,
|
||||
node
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -585,12 +618,12 @@ export function createChecker(program: Program) {
|
|||
}
|
||||
|
||||
function checkProgram(program: Program) {
|
||||
reportDuplicateSymbols(program.globalNamespace.exports!);
|
||||
program.reportDuplicateSymbols(program.globalNamespace.exports!);
|
||||
for (const file of program.sourceFiles) {
|
||||
reportDuplicateSymbols(file.locals!);
|
||||
program.reportDuplicateSymbols(file.locals!);
|
||||
for (const ns of file.namespaces) {
|
||||
reportDuplicateSymbols(ns.locals!);
|
||||
reportDuplicateSymbols(ns.exports!);
|
||||
program.reportDuplicateSymbols(ns.locals!);
|
||||
program.reportDuplicateSymbols(ns.exports!);
|
||||
|
||||
initializeTypeForNamespace(ns);
|
||||
}
|
||||
|
@ -688,7 +721,8 @@ export function createChecker(program: Program) {
|
|||
|
||||
for (const newProp of newProperties) {
|
||||
if (properties.has(newProp.name)) {
|
||||
throwDiagnostic(`Model already has a property named ${newProp.name}`, node);
|
||||
program.reportDiagnostic(`Model already has a property named ${newProp.name}`, node);
|
||||
continue;
|
||||
}
|
||||
|
||||
properties.set(newProp.name, newProp);
|
||||
|
@ -700,15 +734,20 @@ export function createChecker(program: Program) {
|
|||
}
|
||||
|
||||
function checkClassHeritage(heritage: ReferenceExpression[]): ModelType[] {
|
||||
return heritage.map((heritageRef) => {
|
||||
let baseModels = [];
|
||||
for (let heritageRef of heritage) {
|
||||
const heritageType = getTypeForNode(heritageRef);
|
||||
|
||||
if (heritageType.kind !== "Model") {
|
||||
throwDiagnostic("Models must extend other models.", heritageRef);
|
||||
if (isErrorType(heritageType)) {
|
||||
compilerAssert(program.hasError(), "Should already have reported an error.", heritageRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
return heritageType;
|
||||
});
|
||||
if (heritageType.kind !== "Model") {
|
||||
program.reportDiagnostic("Models must extend other models.", heritageRef);
|
||||
continue;
|
||||
}
|
||||
baseModels.push(heritageType);
|
||||
}
|
||||
return baseModels;
|
||||
}
|
||||
|
||||
function checkSpreadProperty(targetNode: ReferenceExpression): ModelTypeProperty[] {
|
||||
|
@ -717,7 +756,8 @@ export function createChecker(program: Program) {
|
|||
|
||||
if (targetType.kind != "TemplateParameter") {
|
||||
if (targetType.kind !== "Model") {
|
||||
throwDiagnostic("Cannot spread properties of non-model type.", targetNode);
|
||||
program.reportDiagnostic("Cannot spread properties of non-model type.", targetNode);
|
||||
return props;
|
||||
}
|
||||
|
||||
// copy each property
|
||||
|
@ -852,3 +892,7 @@ export function createChecker(program: Program) {
|
|||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function isErrorType(type: Type): type is ErrorType {
|
||||
return type.kind === "Intrinsic" && type.name === "ErrorType";
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import yargs from "yargs";
|
|||
import { CompilerOptions } from "../compiler/options.js";
|
||||
import { compile } from "../compiler/program.js";
|
||||
import { loadADLConfigInDir } from "../config/index.js";
|
||||
import { compilerAssert, DiagnosticError, dumpError, logDiagnostics } from "./diagnostics.js";
|
||||
import { compilerAssert, dumpError, logDiagnostics } from "./diagnostics.js";
|
||||
import { formatADLFiles } from "./formatter.js";
|
||||
import { adlVersion, NodeHost } from "./util.js";
|
||||
|
||||
|
@ -107,22 +107,15 @@ const args = yargs(process.argv.slice(2))
|
|||
.demandCommand(1, "You must use one of the supported commands.").argv;
|
||||
|
||||
async function compileInput(compilerOptions: CompilerOptions, printSuccess = true) {
|
||||
try {
|
||||
await compile(args.path!, NodeHost, compilerOptions);
|
||||
if (printSuccess) {
|
||||
console.log(
|
||||
`Compilation completed successfully, output files are in ${compilerOptions.outputPath}.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DiagnosticError) {
|
||||
logDiagnostics(err.diagnostics, console.error);
|
||||
if (args.debug) {
|
||||
console.error(`Stack trace:\n\n${err.stack}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
throw err; // let non-diagnostic errors go to top-level bug handler.
|
||||
const program = await compile(args.path!, NodeHost, compilerOptions);
|
||||
logDiagnostics(program.diagnostics, console.error);
|
||||
if (program.hasError()) {
|
||||
process.exit(1);
|
||||
}
|
||||
if (printSuccess) {
|
||||
console.log(
|
||||
`Compilation completed successfully, output files are in ${compilerOptions.outputPath}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,22 +259,15 @@ async function printInfo() {
|
|||
const cwd = process.cwd();
|
||||
console.log(`Module: ${url.fileURLToPath(import.meta.url)}`);
|
||||
|
||||
try {
|
||||
const config = await loadADLConfigInDir(cwd);
|
||||
const jsyaml = await import("js-yaml");
|
||||
console.log(`User Config: ${config.filename ?? "No config file found"}`);
|
||||
console.log("-----------");
|
||||
console.log(jsyaml.dump(config));
|
||||
console.log("-----------");
|
||||
} catch (err) {
|
||||
if (err instanceof DiagnosticError) {
|
||||
logDiagnostics(err.diagnostics, console.error);
|
||||
if (args.debug) {
|
||||
console.error(`Stack trace:\n\n${err.stack}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
throw err; // let non-diagnostic errors go to top-level bug handler.
|
||||
const config = await loadADLConfigInDir(cwd);
|
||||
const jsyaml = await import("js-yaml");
|
||||
console.log(`User Config: ${config.filename ?? "No config file found"}`);
|
||||
console.log("-----------");
|
||||
console.log(jsyaml.dump(config));
|
||||
console.log("-----------");
|
||||
logDiagnostics(config.diagnostics, console.error);
|
||||
if (config.diagnostics.some((d) => d.severity === "error")) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,19 +5,6 @@ import { Diagnostic, Node, SourceFile, SourceLocation, Sym, SyntaxKind, Type } f
|
|||
|
||||
export { Message } from "./messages.js";
|
||||
|
||||
/**
|
||||
* Represents an error in the code input that is fatal and bails the compilation.
|
||||
*
|
||||
* This isn't meant to be kept long term, but we currently do this on all errors.
|
||||
*/
|
||||
export class DiagnosticError extends Error {
|
||||
constructor(public readonly diagnostics: readonly Diagnostic[]) {
|
||||
super("Code diagnostics. See diagnostics array.");
|
||||
// Tests don't have our catch-all handler so log the diagnostic now.
|
||||
logVerboseTestOutput((log) => logDiagnostics(diagnostics, log));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a failure with multiple errors.
|
||||
*/
|
||||
|
@ -35,22 +22,7 @@ export class AggregateError extends Error {
|
|||
export const NoTarget = Symbol("NoTarget");
|
||||
export type DiagnosticTarget = Node | Type | Sym | SourceLocation;
|
||||
export type WriteLine = (text?: string) => void;
|
||||
|
||||
export type ErrorHandler = (
|
||||
message: Message | string,
|
||||
target: DiagnosticTarget,
|
||||
args?: (string | number)[]
|
||||
) => void;
|
||||
|
||||
export const throwOnError: ErrorHandler = throwDiagnostic;
|
||||
|
||||
export function throwDiagnostic(
|
||||
message: Message | string,
|
||||
target: DiagnosticTarget | typeof NoTarget,
|
||||
args?: (string | number)[]
|
||||
): never {
|
||||
throw new DiagnosticError([createDiagnostic(message, target, args)]);
|
||||
}
|
||||
export type DiagnosticHandler = (diagnostic: Diagnostic) => void;
|
||||
|
||||
export function createDiagnostic(
|
||||
message: Message | string,
|
||||
|
@ -82,7 +54,10 @@ export function createDiagnostic(
|
|||
};
|
||||
|
||||
if (locationError || formatError) {
|
||||
throw new AggregateError(new DiagnosticError([diagnostic]), locationError, formatError);
|
||||
const diagnosticError = new Error(
|
||||
"Error(s) occurred trying to report diagnostic: " + diagnostic.message
|
||||
);
|
||||
throw new AggregateError(diagnosticError, locationError, formatError);
|
||||
}
|
||||
|
||||
return diagnostic;
|
||||
|
@ -206,10 +181,7 @@ export function logVerboseTestOutput(messageOrCallback: string | ((log: WriteLin
|
|||
}
|
||||
|
||||
export function dumpError(error: Error, writeLine: WriteLine) {
|
||||
if (error instanceof DiagnosticError) {
|
||||
logDiagnostics(error.diagnostics, writeLine);
|
||||
writeLine(error.stack);
|
||||
} else if (error instanceof AggregateError) {
|
||||
if (error instanceof AggregateError) {
|
||||
for (const inner of error.errors) {
|
||||
dumpError(inner, writeLine);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createSymbolTable } from "./binder.js";
|
||||
import { compilerAssert, createDiagnostic, DiagnosticTarget, Message } from "./diagnostics.js";
|
||||
import { compilerAssert, createDiagnostic } from "./diagnostics.js";
|
||||
import {
|
||||
createScanner,
|
||||
isComment,
|
||||
|
@ -1028,19 +1028,14 @@ export function parse(code: string | SourceFile, options: ParseOptions = {}): AD
|
|||
return;
|
||||
}
|
||||
realPositionOfLastError = realPos;
|
||||
|
||||
reportDiagnostic(message, location);
|
||||
const diagnostic = createDiagnostic(message, location);
|
||||
reportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
function reportDiagnostic(
|
||||
message: Message | string,
|
||||
target: DiagnosticTarget,
|
||||
args?: (string | number)[]
|
||||
) {
|
||||
if (typeof message === "string" || message.severity === "error") {
|
||||
function reportDiagnostic(diagnostic: Diagnostic) {
|
||||
if (diagnostic.severity === "error") {
|
||||
parseErrorInNextFinishedNode = true;
|
||||
}
|
||||
const diagnostic = createDiagnostic(message, target, args);
|
||||
parseDiagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { dirname, extname, isAbsolute, join, resolve } from "path";
|
|||
import resolveModule from "resolve";
|
||||
import { createBinder, createSymbolTable } from "./binder.js";
|
||||
import { createChecker } from "./checker.js";
|
||||
import { createSourceFile, DiagnosticError, NoTarget, throwDiagnostic } from "./diagnostics.js";
|
||||
import { createDiagnostic, createSourceFile, DiagnosticTarget, NoTarget } from "./diagnostics.js";
|
||||
import { Message } from "./messages.js";
|
||||
import { CompilerOptions } from "./options.js";
|
||||
import { parse } from "./parser.js";
|
||||
|
@ -11,11 +11,14 @@ import {
|
|||
CompilerHost,
|
||||
DecoratorExpressionNode,
|
||||
DecoratorSymbol,
|
||||
Diagnostic,
|
||||
IdentifierNode,
|
||||
LiteralType,
|
||||
ModelStatementNode,
|
||||
ModelType,
|
||||
NamespaceStatementNode,
|
||||
Sym,
|
||||
SymbolTable,
|
||||
SyntaxKind,
|
||||
Type,
|
||||
} from "./types.js";
|
||||
|
@ -27,6 +30,7 @@ export interface Program {
|
|||
literalTypes: Map<string | number | boolean, LiteralType>;
|
||||
host: CompilerHost;
|
||||
checker?: ReturnType<typeof createChecker>;
|
||||
readonly diagnostics: readonly Diagnostic[];
|
||||
evalAdlScript(adlScript: string, filePath?: string): void;
|
||||
onBuild(cb: (program: Program) => void): Promise<void> | void;
|
||||
getOption(key: string): string | undefined;
|
||||
|
@ -35,6 +39,15 @@ export interface Program {
|
|||
executeDecorator(node: DecoratorExpressionNode, program: Program, type: Type): void;
|
||||
stateSet(key: Symbol): Set<any>;
|
||||
stateMap(key: Symbol): Map<any, any>;
|
||||
hasError(): boolean;
|
||||
reportDiagnostic(
|
||||
message: Message | string,
|
||||
target: DiagnosticTarget | typeof NoTarget,
|
||||
args?: (string | number)[]
|
||||
): void;
|
||||
reportDiagnostic(diagnostic: Diagnostic): void;
|
||||
reportDiagnostics(diagnostics: Diagnostic[]): void;
|
||||
reportDuplicateSymbols(symbols: SymbolTable): void;
|
||||
}
|
||||
|
||||
export async function createProgram(
|
||||
|
@ -44,14 +57,18 @@ export async function createProgram(
|
|||
const buildCbs: any = [];
|
||||
const stateMaps = new Map<Symbol, Map<any, any>>();
|
||||
const stateSets = new Map<Symbol, Set<any>>();
|
||||
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const seenSourceFiles = new Set<string>();
|
||||
const duplicateSymbols = new Set<Sym>();
|
||||
let error = false;
|
||||
|
||||
const program: Program = {
|
||||
compilerOptions: options || {},
|
||||
globalNamespace: createGlobalNamespace(),
|
||||
sourceFiles: [],
|
||||
literalTypes: new Map(),
|
||||
host,
|
||||
diagnostics,
|
||||
evalAdlScript,
|
||||
executeModelDecorators,
|
||||
executeDecorators,
|
||||
|
@ -59,13 +76,19 @@ export async function createProgram(
|
|||
getOption,
|
||||
stateMap,
|
||||
stateSet,
|
||||
reportDiagnostic,
|
||||
reportDiagnostics,
|
||||
reportDuplicateSymbols,
|
||||
hasError() {
|
||||
return error;
|
||||
},
|
||||
onBuild(cb) {
|
||||
buildCbs.push(cb);
|
||||
},
|
||||
};
|
||||
|
||||
let virtualFileCount = 0;
|
||||
const binder = createBinder();
|
||||
const binder = createBinder(program.reportDuplicateSymbols);
|
||||
|
||||
if (!options?.nostdlib) {
|
||||
await loadStandardLibrary(program);
|
||||
|
@ -131,14 +154,16 @@ export async function createProgram(
|
|||
|
||||
function executeDecorator(dec: DecoratorExpressionNode, program: Program, type: Type) {
|
||||
if (dec.target.kind !== SyntaxKind.Identifier) {
|
||||
throwDiagnostic("Decorator must be identifier", dec);
|
||||
program.reportDiagnostic("Decorator must be identifier", dec);
|
||||
return;
|
||||
}
|
||||
|
||||
const decName = dec.target.sv;
|
||||
const args = dec.arguments.map((a) => toJSON(checker.getTypeForNode(a)));
|
||||
const decBinding = program.globalNamespace.locals!.get(decName) as DecoratorSymbol;
|
||||
if (!decBinding) {
|
||||
throwDiagnostic(`Can't find decorator ${decName}`, dec);
|
||||
program.reportDiagnostic(`Can't find decorator ${decName}`, dec);
|
||||
return;
|
||||
}
|
||||
const decFn = decBinding.value;
|
||||
decFn(program, type, ...args);
|
||||
|
@ -219,11 +244,7 @@ export async function createProgram(
|
|||
const unparsedFile = createSourceFile(adlScript, filePath);
|
||||
const sourceFile = parse(unparsedFile);
|
||||
|
||||
// We don't attempt to evaluate yet when there are parse errors.
|
||||
if (sourceFile.parseDiagnostics.length > 0) {
|
||||
throw new DiagnosticError(sourceFile.parseDiagnostics);
|
||||
}
|
||||
|
||||
program.reportDiagnostics(sourceFile.parseDiagnostics);
|
||||
program.sourceFiles.push(sourceFile);
|
||||
binder.bindSourceFile(program, sourceFile);
|
||||
await evalImports(sourceFile);
|
||||
|
@ -247,7 +268,8 @@ export async function createProgram(
|
|||
target = await resolveModuleSpecifier(path, basedir);
|
||||
} catch (e) {
|
||||
if (e.code === "MODULE_NOT_FOUND") {
|
||||
throwDiagnostic(`Couldn't find library "${path}"`, stmt);
|
||||
program.reportDiagnostic(`Couldn't find library "${path}"`, stmt);
|
||||
continue;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
@ -264,7 +286,7 @@ export async function createProgram(
|
|||
} else if (ext === ".adl") {
|
||||
await loadAdlFile(target);
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
program.reportDiagnostic(
|
||||
"Import paths must reference either a directory, a .adl file, or .js file",
|
||||
stmt
|
||||
);
|
||||
|
@ -351,6 +373,9 @@ export async function createProgram(
|
|||
const mainPath = resolve(host.getCwd(), options.mainFile);
|
||||
|
||||
const mainStat = await getMainPathStats(mainPath);
|
||||
if (!mainStat) {
|
||||
return;
|
||||
}
|
||||
if (mainStat.isDirectory()) {
|
||||
await loadDirectory(mainPath);
|
||||
} else {
|
||||
|
@ -363,7 +388,8 @@ export async function createProgram(
|
|||
return await host.stat(mainPath);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
throwDiagnostic(Message.FileNotFound, NoTarget, [mainPath]);
|
||||
program.reportDiagnostic(Message.FileNotFound, NoTarget, [mainPath]);
|
||||
return undefined;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
@ -392,8 +418,49 @@ export async function createProgram(
|
|||
|
||||
return s;
|
||||
}
|
||||
|
||||
function reportDiagnostic(diagnostic: Diagnostic): void;
|
||||
|
||||
function reportDiagnostic(
|
||||
message: Message | string,
|
||||
target: DiagnosticTarget | typeof NoTarget,
|
||||
args?: (string | number)[]
|
||||
): void;
|
||||
|
||||
function reportDiagnostic(
|
||||
diagnostic: Message | string | Diagnostic,
|
||||
target?: DiagnosticTarget | typeof NoTarget,
|
||||
args?: (string | number)[]
|
||||
): void {
|
||||
if (typeof diagnostic === "string" || "text" in diagnostic) {
|
||||
diagnostic = createDiagnostic(diagnostic, target!, args);
|
||||
}
|
||||
if (diagnostic.severity === "error") {
|
||||
error = true;
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
function reportDiagnostics(newDiagnostics: Diagnostic[]) {
|
||||
for (const diagnostic of newDiagnostics) {
|
||||
reportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
function reportDuplicateSymbols(symbols: SymbolTable) {
|
||||
for (const symbol of symbols.duplicates) {
|
||||
if (!duplicateSymbols.has(symbol)) {
|
||||
duplicateSymbols.add(symbol);
|
||||
reportDiagnostic("Duplicate name: " + symbol.name, symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function compile(rootDir: string, host: CompilerHost, options?: CompilerOptions) {
|
||||
const program = await createProgram(host, { mainFile: rootDir, ...options });
|
||||
export async function compile(
|
||||
rootDir: string,
|
||||
host: CompilerHost,
|
||||
options?: CompilerOptions
|
||||
): Promise<Program> {
|
||||
return await createProgram(host, { mainFile: rootDir, ...options });
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
isWhiteSpaceSingleLine,
|
||||
utf16CodeUnits,
|
||||
} from "./charcode.js";
|
||||
import { createSourceFile, Message, throwOnError } from "./diagnostics.js";
|
||||
import { createDiagnostic, createSourceFile, DiagnosticHandler, Message } from "./diagnostics.js";
|
||||
import { SourceFile } from "./types.js";
|
||||
|
||||
// All conflict markers consist of the same character repeated seven times. If it is
|
||||
|
@ -244,7 +244,10 @@ export function isStatementKeyword(token: Token) {
|
|||
return token >= MinStatementKeyword && token <= MaxStatementKeyword;
|
||||
}
|
||||
|
||||
export function createScanner(source: string | SourceFile, onError = throwOnError): Scanner {
|
||||
export function createScanner(
|
||||
source: string | SourceFile,
|
||||
diagnosticHandler: DiagnosticHandler
|
||||
): Scanner {
|
||||
const file = typeof source === "string" ? createSourceFile(source, "<anonymous file>") : source;
|
||||
const input = file.text;
|
||||
let position = 0;
|
||||
|
@ -479,7 +482,8 @@ export function createScanner(source: string | SourceFile, onError = throwOnErro
|
|||
}
|
||||
|
||||
function error(msg: Message, args?: (string | number)[]) {
|
||||
onError(msg, { file, pos: tokenPosition, end: position }, args);
|
||||
const diagnostic = createDiagnostic(msg, { file, pos: tokenPosition, end: position }, args);
|
||||
diagnosticHandler(diagnostic);
|
||||
}
|
||||
|
||||
function scanWhitespace(): Token {
|
||||
|
|
|
@ -28,6 +28,10 @@ export interface IntrinsicType extends BaseType {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export interface ErrorType extends IntrinsicType {
|
||||
name: "ErrorType";
|
||||
}
|
||||
|
||||
export interface ModelType extends BaseType {
|
||||
kind: "Model";
|
||||
name: string;
|
||||
|
|
|
@ -2,8 +2,7 @@ import fs from "fs";
|
|||
import { readdir, readFile, realpath, stat, writeFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { fileURLToPath, pathToFileURL, URL } from "url";
|
||||
import { createDiagnostic, DiagnosticError } from "./diagnostics.js";
|
||||
import { CompilerHost, Diagnostic, Sym, SymbolTable } from "./types.js";
|
||||
import { CompilerHost } from "./types.js";
|
||||
|
||||
export const adlVersion = getVersion();
|
||||
|
||||
|
@ -13,35 +12,6 @@ function getVersion(): string {
|
|||
return packageJson.version;
|
||||
}
|
||||
|
||||
export function reportDuplicateSymbols(symbols: SymbolTable) {
|
||||
let reported = new Set<Sym>();
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
|
||||
for (const symbol of symbols.duplicates) {
|
||||
report(symbol);
|
||||
}
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
// TODO: We're now reporting all duplicates up to the binding of the first file
|
||||
// that introduced one, but still bailing the compilation rather than
|
||||
// recovering and reporting other issues including the possibility of more
|
||||
// duplicates.
|
||||
//
|
||||
// That said, decorators are entered into the global symbol table before
|
||||
// any source file is bound and therefore this will include all duplicate
|
||||
// decorator implementations.
|
||||
throw new DiagnosticError(diagnostics);
|
||||
}
|
||||
|
||||
function report(symbol: Sym) {
|
||||
if (!reported.has(symbol)) {
|
||||
reported.add(symbol);
|
||||
const diagnostic = createDiagnostic("Duplicate name: " + symbol.name, symbol);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deepFreeze<T>(value: T): T {
|
||||
if (Array.isArray(value)) {
|
||||
value.map(deepFreeze);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { readFile } from "fs/promises";
|
||||
import { basename, extname, join } from "path";
|
||||
import { createSourceFile, throwDiagnostic } from "../compiler/diagnostics.js";
|
||||
import { createDiagnostic, createSourceFile } from "../compiler/diagnostics.js";
|
||||
import { Diagnostic } from "../compiler/types.js";
|
||||
import { deepClone, deepFreeze } from "../compiler/util.js";
|
||||
import { ConfigValidator } from "./config-validator.js";
|
||||
import { ADLConfig } from "./types.js";
|
||||
|
@ -8,6 +9,7 @@ import { ADLConfig } from "./types.js";
|
|||
const configFilenames = [".adlrc.yaml", ".adlrc.yml", ".adlrc.json", "package.json"];
|
||||
const defaultConfig: ADLConfig = deepFreeze({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
emitters: {},
|
||||
lint: {
|
||||
extends: [],
|
||||
|
@ -79,17 +81,32 @@ async function loadConfigFile(
|
|||
const content = await readFile(filePath, "utf-8");
|
||||
const file = createSourceFile(content, filePath);
|
||||
|
||||
let config: any;
|
||||
let loadDiagnostics: Diagnostic[];
|
||||
let data: any;
|
||||
try {
|
||||
config = loadData(content);
|
||||
data = loadData(content);
|
||||
loadDiagnostics = [];
|
||||
} catch (e) {
|
||||
throwDiagnostic(e.message, { file, pos: 0, end: 0 });
|
||||
loadDiagnostics = [createDiagnostic(e.message, { file, pos: 0, end: 0 })];
|
||||
}
|
||||
|
||||
configValidator.validateConfig(config, file);
|
||||
mergeDefaults(config, defaultConfig);
|
||||
config.filename = filePath;
|
||||
return config;
|
||||
const validationDiagnostics = configValidator.validateConfig(data);
|
||||
const diagnostics = [...loadDiagnostics, ...validationDiagnostics];
|
||||
|
||||
if (diagnostics.some((d) => d.severity === "error")) {
|
||||
// NOTE: Don't trust the data if there are validation errors, and use
|
||||
// default config. Otherwise, we may return an object that does not
|
||||
// conform to ADLConfig's typing.
|
||||
data = defaultConfig;
|
||||
} else {
|
||||
mergeDefaults(data, defaultConfig);
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
filename: filePath,
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Ajv, { ErrorObject } from "ajv";
|
||||
import { DiagnosticError } from "../compiler/diagnostics.js";
|
||||
import { compilerAssert } from "../compiler/diagnostics.js";
|
||||
import { Diagnostic, SourceFile } from "../compiler/types.js";
|
||||
import { ADLConfigJsonSchema } from "./config-schema.js";
|
||||
import { ADLRawConfig } from "./types.js";
|
||||
|
@ -13,19 +13,16 @@ export class ConfigValidator {
|
|||
* Validate the config is valid
|
||||
* @param config Configuration
|
||||
* @param file @optional file for errors tracing.
|
||||
* @returns
|
||||
* @returns Validation
|
||||
*/
|
||||
public validateConfig(config: ADLRawConfig, file?: SourceFile) {
|
||||
public validateConfig(config: ADLRawConfig, file?: SourceFile): Diagnostic[] {
|
||||
const validate = this.ajv.compile(ADLConfigJsonSchema);
|
||||
const valid = validate(config);
|
||||
|
||||
if (!valid && validate.errors) {
|
||||
throw new DiagnosticError(
|
||||
validate.errors.map((error) => {
|
||||
return ajvErrorToDiagnostic(error, file);
|
||||
})
|
||||
);
|
||||
}
|
||||
compilerAssert(
|
||||
!valid || !validate.errors,
|
||||
"There should be errors reported if the config file is not valid."
|
||||
);
|
||||
return validate.errors?.map((e) => ajvErrorToDiagnostic(e, file)) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { Diagnostic } from "../compiler";
|
||||
|
||||
/**
|
||||
* Represent the normalized user configuration.
|
||||
*/
|
||||
|
@ -7,6 +9,11 @@ export interface ADLConfig {
|
|||
*/
|
||||
filename?: string;
|
||||
|
||||
/**
|
||||
* Diagnostics reported while loading the configuration
|
||||
*/
|
||||
diagnostics: Diagnostic[];
|
||||
|
||||
plugins: string[];
|
||||
lint: ADLLintConfig;
|
||||
emitters: Record<string, boolean>;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { throwDiagnostic } from "../compiler/diagnostics.js";
|
||||
import { Program } from "../compiler/program.js";
|
||||
import { ModelTypeProperty, NamespaceType, Type } from "../compiler/types.js";
|
||||
|
||||
|
@ -26,7 +25,10 @@ export function intrinsic(program: Program, target: Type) {
|
|||
program.stateSet(intrinsicsKey).add(target);
|
||||
}
|
||||
|
||||
export function isIntrinsic(program: Program, target: Type) {
|
||||
export function isIntrinsic(program: Program, target: Type | undefined) {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
return program.stateSet(intrinsicsKey).has(target);
|
||||
}
|
||||
|
||||
|
@ -56,13 +58,14 @@ export function getIntrinsicType(program: Program, target: Type | undefined): st
|
|||
const numericTypesKey = Symbol();
|
||||
export function numeric(program: Program, target: Type) {
|
||||
if (!isIntrinsic(program, target)) {
|
||||
throwDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target);
|
||||
program.reportDiagnostic("Cannot apply @numeric decorator to non-intrinsic type.", target);
|
||||
return;
|
||||
}
|
||||
if (target.kind === "Model") {
|
||||
program.stateSet(numericTypesKey).add(target.name);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @numeric decorator to non-model type.", target);
|
||||
if (target.kind !== "Model") {
|
||||
program.reportDiagnostic("Cannot apply @numeric decorator to non-model type.", target);
|
||||
return;
|
||||
}
|
||||
program.stateSet(numericTypesKey).add(target.name);
|
||||
}
|
||||
|
||||
export function isNumericType(program: Program, target: Type): boolean {
|
||||
|
@ -75,16 +78,20 @@ export function isNumericType(program: Program, target: Type): boolean {
|
|||
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(program, target) === "string") {
|
||||
program.stateMap(formatValuesKey).set(target, format);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @format to a non-string type", target);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @format to anything that isn't a Model or ModelProperty", target);
|
||||
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"Cannot apply @format to anything that isn't a Model or ModelProperty",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getIntrinsicType(program, target) !== "string") {
|
||||
program.reportDiagnostic("Cannot apply @format to a non-string type", target);
|
||||
return;
|
||||
}
|
||||
|
||||
program.stateMap(formatValuesKey).set(target, format);
|
||||
}
|
||||
|
||||
export function getFormat(program: Program, target: Type): string | undefined {
|
||||
|
@ -96,19 +103,20 @@ export function getFormat(program: Program, target: Type): string | undefined {
|
|||
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(program, target) === "string") {
|
||||
program.stateMap(minLengthValuesKey).set(target, minLength);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @minLength to a non-string type", target);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"Cannot apply @minLength to anything that isn't a Model or ModelProperty",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getIntrinsicType(program, target) !== "string") {
|
||||
program.reportDiagnostic("Cannot apply @minLength to a non-string type", target);
|
||||
return;
|
||||
}
|
||||
|
||||
program.stateMap(minLengthValuesKey).set(target, minLength);
|
||||
}
|
||||
|
||||
export function getMinLength(program: Program, target: Type): number | undefined {
|
||||
|
@ -120,19 +128,19 @@ export function getMinLength(program: Program, target: Type): number | undefined
|
|||
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(program, target) === "string") {
|
||||
program.stateMap(maxLengthValuesKey).set(target, maxLength);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @maxLength to a non-string type", target);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"Cannot apply @maxLength to anything that isn't a Model or ModelProperty",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getIntrinsicType(program, target) !== "string") {
|
||||
program.reportDiagnostic("Cannot apply @maxLength to a non-string type", target);
|
||||
return;
|
||||
}
|
||||
program.stateMap(maxLengthValuesKey).set(target, maxLength);
|
||||
}
|
||||
|
||||
export function getMaxLength(program: Program, target: Type): number | undefined {
|
||||
|
@ -144,19 +152,17 @@ export function getMaxLength(program: Program, target: Type): number | undefined
|
|||
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(program, target)) {
|
||||
program.stateMap(minValuesKey).set(target, minValue);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @minValue to a non-numeric type", target);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"Cannot apply @minValue to anything that isn't a Model or ModelProperty",
|
||||
target
|
||||
);
|
||||
}
|
||||
if (!isNumericType(program, target)) {
|
||||
program.reportDiagnostic("Cannot apply @minValue to a non-numeric type", target);
|
||||
return;
|
||||
}
|
||||
program.stateMap(minValuesKey).set(target, minValue);
|
||||
}
|
||||
|
||||
export function getMinValue(program: Program, target: Type): number | undefined {
|
||||
|
@ -168,19 +174,18 @@ export function getMinValue(program: Program, target: Type): number | undefined
|
|||
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(program, target)) {
|
||||
program.stateMap(maxValuesKey).set(target, maxValue);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @maxValue to a non-numeric type", target);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
if (target.kind !== "Model" && target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"Cannot apply @maxValue to anything that isn't a Model or ModelProperty",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isNumericType(program, target)) {
|
||||
program.reportDiagnostic("Cannot apply @maxValue to a non-numeric type", target);
|
||||
return;
|
||||
}
|
||||
program.stateMap(maxValuesKey).set(target, maxValue);
|
||||
}
|
||||
|
||||
export function getMaxValue(program: Program, target: Type): number | undefined {
|
||||
|
@ -192,16 +197,16 @@ export function getMaxValue(program: Program, target: Type): number | undefined
|
|||
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(program, target) === "string") {
|
||||
program.stateMap(secretTypesKey).set(target, true);
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @secret to a non-string type", target);
|
||||
}
|
||||
} else {
|
||||
throwDiagnostic("Cannot apply @secret to anything that isn't a Model", target);
|
||||
if (target.kind !== "Model") {
|
||||
program.reportDiagnostic("Cannot apply @secret to anything that isn't a Model", target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getIntrinsicType(program, target) !== "string") {
|
||||
program.reportDiagnostic("Cannot apply @secret to a non-string type", target);
|
||||
return;
|
||||
}
|
||||
program.stateMap(secretTypesKey).set(target, true);
|
||||
}
|
||||
|
||||
export function isSecret(program: Program, target: Type): boolean | undefined {
|
||||
|
@ -213,11 +218,14 @@ export function isSecret(program: Program, target: Type): boolean | undefined {
|
|||
const visibilitySettingsKey = Symbol();
|
||||
|
||||
export function visibility(program: Program, target: Type, ...visibilities: string[]) {
|
||||
if (target.kind === "ModelProperty") {
|
||||
program.stateMap(visibilitySettingsKey).set(target, visibilities);
|
||||
} else {
|
||||
throwDiagnostic("The @visibility decorator can only be applied to model properties.", target);
|
||||
if (target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"The @visibility decorator can only be applied to model properties.",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
program.stateMap(visibilitySettingsKey).set(target, visibilities);
|
||||
}
|
||||
|
||||
export function getVisibility(program: Program, target: Type): string[] | undefined {
|
||||
|
@ -226,7 +234,11 @@ export function getVisibility(program: Program, target: Type): string[] | undefi
|
|||
|
||||
export function withVisibility(program: Program, target: Type, ...visibilities: string[]) {
|
||||
if (target.kind !== "Model") {
|
||||
throwDiagnostic("The @withVisibility decorator can only be applied to models.", target);
|
||||
program.reportDiagnostic(
|
||||
"The @withVisibility decorator can only be applied to models.",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = (_: any, prop: ModelTypeProperty) => {
|
||||
|
@ -253,14 +265,14 @@ function mapFilterOut(
|
|||
const listPropertiesKey = Symbol();
|
||||
|
||||
export function list(program: Program, target: Type) {
|
||||
if (target.kind === "Operation" || target.kind === "ModelProperty") {
|
||||
program.stateSet(listPropertiesKey).add(target);
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
"The @list decorator can only be applied to interface or model properties.",
|
||||
if (target.kind !== "Operation" && target.kind !== "ModelProperty") {
|
||||
program.reportDiagnostic(
|
||||
"The @list decorator can only be applied to operations or model properties.",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
program.stateSet(listPropertiesKey).add(target);
|
||||
}
|
||||
|
||||
export function isList(program: Program, target: Type): boolean {
|
||||
|
@ -273,15 +285,18 @@ 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 = program.stateMap(tagPropertiesKey).get(target);
|
||||
if (tags) {
|
||||
tags.push(tag);
|
||||
} else {
|
||||
program.stateMap(tagPropertiesKey).set(target, [tag]);
|
||||
}
|
||||
if (target.kind !== "Operation" && target.kind !== "Namespace") {
|
||||
program.reportDiagnostic(
|
||||
"The @tag decorator can only be applied to namespaces or operations.",
|
||||
target
|
||||
);
|
||||
return;
|
||||
}
|
||||
const tags = program.stateMap(tagPropertiesKey).get(target);
|
||||
if (tags) {
|
||||
tags.push(tag);
|
||||
} else {
|
||||
throwDiagnostic("The @tag decorator can only be applied to namespace or operation.", target);
|
||||
program.stateMap(tagPropertiesKey).set(target, [tag]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { match, strictEqual } from "assert";
|
||||
import { createTestHost, TestHost } from "../test-host.js";
|
||||
|
||||
describe("adl: semantic checks on source with parse errors", () => {
|
||||
let testHost: TestHost;
|
||||
|
||||
beforeEach(async () => {
|
||||
testHost = await createTestHost();
|
||||
});
|
||||
|
||||
it("reports semantic errors in addition to parse errors", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`model M extends Q {
|
||||
a: B;
|
||||
a: C;
|
||||
`
|
||||
);
|
||||
|
||||
const diagnostics = await testHost.diagnose("/");
|
||||
strictEqual(diagnostics.length, 4);
|
||||
match(diagnostics[0].message, /Property expected/);
|
||||
match(diagnostics[1].message, /Unknown identifier Q/);
|
||||
match(diagnostics[2].message, /Unknown identifier B/);
|
||||
match(diagnostics[3].message, /Unknown identifier C/);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import { rejects } from "assert";
|
||||
import { match, strictEqual } from "assert";
|
||||
import { Diagnostic } from "../../compiler/types.js";
|
||||
import { createTestHost, TestHost } from "../test-host.js";
|
||||
|
||||
describe("adl: duplicate declarations", () => {
|
||||
|
@ -8,7 +9,7 @@ describe("adl: duplicate declarations", () => {
|
|||
testHost = await createTestHost();
|
||||
});
|
||||
|
||||
it("throws for duplicate template parameters", async () => {
|
||||
it("reports duplicate template parameters", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
|
@ -16,10 +17,11 @@ describe("adl: duplicate declarations", () => {
|
|||
`
|
||||
);
|
||||
|
||||
await rejects(testHost.compile("/"));
|
||||
const diagnostics = await testHost.diagnose("/");
|
||||
assertDuplicates(diagnostics);
|
||||
});
|
||||
|
||||
it("throws for duplicate model declarations in global scope", async () => {
|
||||
it("reports duplicate model declarations in global scope", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
|
@ -28,10 +30,11 @@ describe("adl: duplicate declarations", () => {
|
|||
`
|
||||
);
|
||||
|
||||
await rejects(testHost.compile("/"));
|
||||
const diagnostics = await testHost.diagnose("/");
|
||||
assertDuplicates(diagnostics);
|
||||
});
|
||||
|
||||
it("throws for duplicate model declarations in a single namespace", async () => {
|
||||
it("reports duplicate model declarations in a single namespace", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
|
@ -41,10 +44,11 @@ describe("adl: duplicate declarations", () => {
|
|||
`
|
||||
);
|
||||
|
||||
await rejects(testHost.compile("/"));
|
||||
const diagnostics = await testHost.diagnose("/");
|
||||
assertDuplicates(diagnostics);
|
||||
});
|
||||
|
||||
it("throws for duplicate model declarations in across multiple namespaces", async () => {
|
||||
it("reports duplicate model declarations across multiple namespaces", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
|
@ -58,10 +62,11 @@ describe("adl: duplicate declarations", () => {
|
|||
`
|
||||
);
|
||||
|
||||
await rejects(testHost.compile("/"));
|
||||
const diagnostics = await testHost.diagnose("/");
|
||||
assertDuplicates(diagnostics);
|
||||
});
|
||||
|
||||
it("throws for duplicate model declarations in across multiple files and namespaces", async () => {
|
||||
it("reports duplicate model declarations across multiple files and namespaces", async () => {
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
|
@ -79,6 +84,14 @@ describe("adl: duplicate declarations", () => {
|
|||
`
|
||||
);
|
||||
|
||||
await rejects(testHost.compile("/"));
|
||||
const diagnostics = await testHost.diagnose("/");
|
||||
assertDuplicates(diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
function assertDuplicates(diagnostics: readonly Diagnostic[]) {
|
||||
strictEqual(diagnostics.length, 2);
|
||||
for (const diagnostic of diagnostics) {
|
||||
match(diagnostic.message, /Duplicate name/);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { deepStrictEqual, throws } from "assert";
|
||||
import { deepStrictEqual } from "assert";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { DiagnosticError } from "../../compiler/diagnostics.js";
|
||||
import { ConfigValidator } from "../../config/config-validator.js";
|
||||
import { loadADLConfigInDir } from "../../config/index.js";
|
||||
|
||||
|
@ -20,6 +19,7 @@ describe("adl: config file loading", () => {
|
|||
const config = await loadTestConfig(folderName);
|
||||
deepStrictEqual(config, {
|
||||
plugins: ["foo"],
|
||||
diagnostics: [],
|
||||
emitters: {
|
||||
"foo:openapi": true,
|
||||
},
|
||||
|
@ -48,6 +48,7 @@ describe("adl: config file loading", () => {
|
|||
const config = await loadTestConfig("empty");
|
||||
deepStrictEqual(config, {
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
emitters: {},
|
||||
lint: {
|
||||
extends: [],
|
||||
|
@ -62,17 +63,16 @@ describe("adl: config file loading", () => {
|
|||
});
|
||||
|
||||
it("deep clones defaults when not found", async () => {
|
||||
// load and mutate
|
||||
let config = await loadTestConfig("empty");
|
||||
config.plugins.push("x");
|
||||
config.emitters["x"] = true;
|
||||
config.lint.extends.push("x");
|
||||
config.lint.rules["x"] = "off";
|
||||
|
||||
// reload and make sure mutation is not observed
|
||||
config = await loadTestConfig("empty");
|
||||
deepStrictEqual(config, {
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
emitters: {},
|
||||
lint: {
|
||||
extends: [],
|
||||
|
@ -82,17 +82,16 @@ describe("adl: config file loading", () => {
|
|||
});
|
||||
|
||||
it("deep clones defaults when found", async () => {
|
||||
// load and mutate
|
||||
let config = await loadTestConfig("yaml");
|
||||
config.plugins.push("x");
|
||||
config.emitters["x"] = true;
|
||||
config.lint.extends.push("x");
|
||||
config.lint.rules["x"] = "off";
|
||||
|
||||
// reload and make sure mutation is not observed
|
||||
config = await loadTestConfig("yaml");
|
||||
deepStrictEqual(config, {
|
||||
plugins: ["foo"],
|
||||
diagnostics: [],
|
||||
emitters: {
|
||||
"foo:openapi": true,
|
||||
},
|
||||
|
@ -110,32 +109,26 @@ describe("adl: config file loading", () => {
|
|||
const validator = new ConfigValidator();
|
||||
|
||||
it("does not allow additional properties", () => {
|
||||
throws(
|
||||
() => validator.validateConfig({ someCustomProp: true } as any),
|
||||
new DiagnosticError([
|
||||
{
|
||||
severity: "error",
|
||||
message:
|
||||
"Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp",
|
||||
},
|
||||
])
|
||||
);
|
||||
deepStrictEqual(validator.validateConfig({ someCustomProp: true } as any), [
|
||||
{
|
||||
severity: "error",
|
||||
message:
|
||||
"Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("fail if passing the wrong type", () => {
|
||||
throws(
|
||||
() => validator.validateConfig({ emitters: true } as any),
|
||||
new DiagnosticError([
|
||||
{
|
||||
severity: "error",
|
||||
message: "Schema violation: must be object (/emitters)",
|
||||
},
|
||||
])
|
||||
);
|
||||
it("fails if passing the wrong type", () => {
|
||||
deepStrictEqual(validator.validateConfig({ emitters: true } as any), [
|
||||
{
|
||||
severity: "error",
|
||||
message: "Schema violation: must be object (/emitters)",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("succeeed if config is valid", () => {
|
||||
validator.validateConfig({ lint: { rules: { foo: "on" } } });
|
||||
it("succeeeds if config is valid", () => {
|
||||
deepStrictEqual(validator.validateConfig({ lint: { rules: { foo: "on" } } }), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
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";
|
||||
import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
|
||||
import { CompilerOptions } from "../compiler/options.js";
|
||||
import { createProgram, Program } from "../compiler/program.js";
|
||||
import { CompilerHost, Diagnostic, Type } from "../compiler/types.js";
|
||||
|
||||
export interface TestHost {
|
||||
addAdlFile(path: string, contents: string): void;
|
||||
|
@ -12,6 +12,11 @@ export interface TestHost {
|
|||
addRealAdlFile(path: string, realPath: string): Promise<void>;
|
||||
addRealJsFile(path: string, realPath: string): Promise<void>;
|
||||
compile(main: string, options?: CompilerOptions): Promise<Record<string, Type>>;
|
||||
diagnose(main: string, options?: CompilerOptions): Promise<readonly Diagnostic[]>;
|
||||
compileAndDiagnose(
|
||||
main: string,
|
||||
options?: CompilerOptions
|
||||
): Promise<[Record<string, Type>, readonly Diagnostic[]]>;
|
||||
testTypes: Record<string, Type>;
|
||||
program: Program;
|
||||
/**
|
||||
|
@ -146,6 +151,8 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
addRealAdlFile,
|
||||
addRealJsFile,
|
||||
compile,
|
||||
diagnose,
|
||||
compileAndDiagnose,
|
||||
testTypes,
|
||||
get program() {
|
||||
return program;
|
||||
|
@ -177,24 +184,34 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
}
|
||||
|
||||
async function compile(main: string, options: CompilerOptions = {}) {
|
||||
const [testTypes, diagnostics] = await compileAndDiagnose(main, options);
|
||||
if (diagnostics.length > 0) {
|
||||
let message = "Unexpected diagnostics:\n" + diagnostics.map(formatDiagnostic).join("\n");
|
||||
throw new Error(message);
|
||||
}
|
||||
return testTypes;
|
||||
}
|
||||
|
||||
async function diagnose(main: string, options: CompilerOptions = {}) {
|
||||
const [, diagnostics] = await compileAndDiagnose(main, options);
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
async function compileAndDiagnose(
|
||||
main: string,
|
||||
options: CompilerOptions = {}
|
||||
): Promise<[Record<string, Type>, readonly Diagnostic[]]> {
|
||||
// default is noEmit
|
||||
if (!options.hasOwnProperty("noEmit")) {
|
||||
options.noEmit = true;
|
||||
}
|
||||
|
||||
try {
|
||||
program = await createProgram(compilerHost, {
|
||||
mainFile: main,
|
||||
...options,
|
||||
});
|
||||
|
||||
return testTypes;
|
||||
} catch (e) {
|
||||
if (e.diagnostics) {
|
||||
throw e.diagnostics;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
program = await createProgram(compilerHost, {
|
||||
mainFile: main,
|
||||
...options,
|
||||
});
|
||||
logVerboseTestOutput((log) => logDiagnostics(program.diagnostics, log));
|
||||
return [testTypes, program.diagnostics];
|
||||
}
|
||||
|
||||
function isContainedIn(a: string, b: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import assert from "assert";
|
||||
import { CharCode } from "../compiler/charcode.js";
|
||||
import { logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
|
||||
import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
|
||||
import { hasParseError, NodeFlags, parse } from "../compiler/parser.js";
|
||||
import { ADLScriptNode, SyntaxKind } from "../compiler/types.js";
|
||||
|
||||
|
@ -435,17 +435,15 @@ function parseEach(cases: (string | [string, Callback])[]) {
|
|||
|
||||
logVerboseTestOutput("\n=== Diagnostics ===");
|
||||
if (astNode.parseDiagnostics.length > 0) {
|
||||
const diagnostics: string[] = [];
|
||||
logDiagnostics(astNode.parseDiagnostics, (e) => diagnostics.push(e!));
|
||||
const diagnostics = astNode.parseDiagnostics.map(formatDiagnostic).join("\n");
|
||||
|
||||
assert.strictEqual(
|
||||
hasParseError(astNode),
|
||||
astNode.parseDiagnostics.some((e) => e.severity === "error"),
|
||||
"root node claims to have no parse errors, but these were reported:\n" +
|
||||
diagnostics.join("\n")
|
||||
"root node claims to have no parse errors, but these were reported:\n" + diagnostics
|
||||
);
|
||||
|
||||
assert.fail("Unexpected parse errors in test:\n" + diagnostics.join("\n"));
|
||||
assert.fail("Unexpected parse errors in test:\n" + diagnostics);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import assert from "assert";
|
|||
import { readFile } from "fs/promises";
|
||||
import { URL } from "url";
|
||||
import { isIdentifierContinue, isIdentifierStart } from "../compiler/charcode.js";
|
||||
import { createDiagnostic, formatDiagnostic, throwOnError } from "../compiler/diagnostics.js";
|
||||
import { DiagnosticHandler, formatDiagnostic } from "../compiler/diagnostics.js";
|
||||
import {
|
||||
createScanner,
|
||||
isKeyword,
|
||||
|
@ -25,8 +25,13 @@ type TokenEntry = [
|
|||
}?
|
||||
];
|
||||
|
||||
function tokens(text: string, onError = throwOnError): TokenEntry[] {
|
||||
const scanner = createScanner(text, onError);
|
||||
function tokens(text: string, diagnosticHandler?: DiagnosticHandler): TokenEntry[] {
|
||||
if (!diagnosticHandler) {
|
||||
diagnosticHandler = (diagnostic) =>
|
||||
assert.fail("Unexpected diagnostic: " + formatDiagnostic(diagnostic));
|
||||
}
|
||||
|
||||
const scanner = createScanner(text, diagnosticHandler);
|
||||
const result: TokenEntry[] = [];
|
||||
do {
|
||||
const token = scanner.scan();
|
||||
|
@ -181,8 +186,7 @@ describe("adl: scanner", () => {
|
|||
});
|
||||
|
||||
function scanString(text: string, expectedValue: string, expectedDiagnostic?: RegExp) {
|
||||
const scanner = createScanner(text, (message, target, args) => {
|
||||
const diagnostic = createDiagnostic(message, target, args);
|
||||
const scanner = createScanner(text, (diagnostic) => {
|
||||
if (expectedDiagnostic) {
|
||||
assert.match(diagnostic.message, expectedDiagnostic);
|
||||
} else {
|
||||
|
|
Загрузка…
Ссылка в новой задаче