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:
Nick Guerrera 2021-06-03 08:46:10 -07:00 коммит произвёл GitHub
Родитель 451274c269
Коммит c62c1036f9
20 изменённых файлов: 480 добавлений и 340 удалений

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

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