Use same entry point handling for compile and import (#577)

We no longer glob files when a directory is passed to `adl
compile`. Instead, we will look for `adlMain` specified by package.json or
main.adl in the directory. This eliminates any dependency on file system
enumeration order and will make it easier for tooling to implement semantic
features.

Also:
* Fix various issues with reporting I/O errors as diagnostics.
* Fix issue where regen-samples would silently pass PR validation if nothing
  at all was emitted.
* Make regen-samples have opt-out exclude list instead of opt-in include
  list.
* Report correct location for syntax errors in decorators, and fix issue
  with copying petstore sample to another folder without type: module in
  package.json
* Allow .mjs extension for decorator imports.
* Make sure unahndled promise rejection becomes internal compiler error.
* Fix issue with dumping config with diagnostics from `adl info`
* Avoid repeating config filename in `adl info`
This commit is contained in:
Nick Guerrera 2021-06-04 14:12:59 -07:00 коммит произвёл GitHub
Родитель c62c1036f9
Коммит 8518a0bd17
21 изменённых файлов: 423 добавлений и 227 удалений

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

@ -16,10 +16,10 @@ const args = yargs(process.argv.slice(2))
.scriptName("adl")
.help()
.strict()
.command("compile <path>", "Compile a directory of ADL files.", (cmd) => {
.command("compile <path>", "Compile ADL source.", (cmd) => {
return cmd
.positional("path", {
description: "The path to folder containing .adl files",
description: "The path to the main.adl file or directory containing main.adl.",
type: "string",
})
.option("output-path", {
@ -40,44 +40,40 @@ const args = yargs(process.argv.slice(2))
describe: "Don't load the ADL standard library.",
});
})
.command(
"generate <path>",
"Generate client and server code from a directory of ADL files.",
(cmd) => {
return (
cmd
.positional("path", {
description: "The path to folder containing .adl files",
type: "string",
})
.option("client", {
type: "boolean",
describe: "Generate a client library for the ADL definition",
})
.option("language", {
type: "string",
choices: ["typescript", "csharp", "python"],
describe: "The language to use for code generation",
})
.option("output-path", {
type: "string",
default: "./adl-output",
describe:
"The output path for generated artifacts. If it does not exist, it will be created.",
})
.option("option", {
type: "array",
string: true,
describe:
"Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.",
})
// we can't generate anything but a client yet
.demandOption("client")
// and language is required to do so
.demandOption("language")
);
}
)
.command("generate <path>", "Generate client code from ADL source.", (cmd) => {
return (
cmd
.positional("path", {
description: "The path to folder containing .adl files",
type: "string",
})
.option("client", {
type: "boolean",
describe: "Generate a client library for the ADL definition",
})
.option("language", {
type: "string",
choices: ["typescript", "csharp", "python"],
describe: "The language to use for code generation",
})
.option("output-path", {
type: "string",
default: "./adl-output",
describe:
"The output path for generated artifacts. If it does not exist, it will be created.",
})
.option("option", {
type: "array",
string: true,
describe:
"Key/value pairs that can be passed to ADL components. The format is 'key=value'. This parameter can be used multiple times to add more options.",
})
// we can't generate anything but a client yet
.demandOption("client")
// and language is required to do so
.demandOption("language")
);
})
.command("code", "Manage VS Code Extension.", (cmd) => {
return cmd
.demandCommand(1, "No command specified.")
@ -261,9 +257,12 @@ async function printInfo() {
const config = await loadADLConfigInDir(cwd);
const jsyaml = await import("js-yaml");
const excluded = ["diagnostics", "filename"];
const replacer = (key: string, value: any) => (excluded.includes(key) ? undefined : value);
console.log(`User Config: ${config.filename ?? "No config file found"}`);
console.log("-----------");
console.log(jsyaml.dump(config));
console.log(jsyaml.dump(config, { replacer }));
console.log("-----------");
logDiagnostics(config.diagnostics, console.error);
if (config.diagnostics.some((d) => d.severity === "error")) {
@ -377,15 +376,20 @@ async function main() {
}
}
main()
.then(() => {})
.catch((err) => {
// NOTE: An expected error, like one thrown for bad input, shouldn't reach
// here, but be handled somewhere else. If we reach here, it should be
// considered a bug and therefore we should not suppress the stack trace as
// that risks losing it in the case of a bug that does not repro easily.
console.error("Internal compiler error!");
console.error("File issue at https://github.com/azure/adl");
dumpError(err, console.error);
process.exit(1);
});
function internalCompilerError(error: Error) {
// NOTE: An expected error, like one thrown for bad input, shouldn't reach
// here, but be handled somewhere else. If we reach here, it should be
// considered a bug and therefore we should not suppress the stack trace as
// that risks losing it in the case of a bug that does not repro easily.
console.error("Internal compiler error!");
console.error("File issue at https://github.com/azure/adl");
dumpError(error, console.error);
process.exit(1);
}
process.on("unhandledRejection", (error: Error) => {
console.error("Unhandled promise rejection!");
internalCompilerError(error);
});
main().catch(internalCompilerError);

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

@ -62,7 +62,7 @@ export const Message = {
FileNotFound: {
code: 1109,
text: `File {0} is not found.`,
text: `File {0} not found.`,
severity: "error",
} as const,
};

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

@ -1,6 +1,5 @@
export interface CompilerOptions {
miscOptions?: any;
mainFile?: string;
outputPath?: string;
swaggerOutputFile?: string;
nostdlib?: boolean;

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

@ -1,4 +1,4 @@
import { dirname, extname, isAbsolute, join, resolve } from "path";
import { dirname, extname, isAbsolute, resolve } from "path";
import resolveModule from "resolve";
import { createBinder, createSymbolTable } from "./binder.js";
import { createChecker } from "./checker.js";
@ -22,6 +22,7 @@ import {
SyntaxKind,
Type,
} from "./types.js";
import { doIO, loadFile } from "./util.js";
export interface Program {
compilerOptions: CompilerOptions;
@ -52,7 +53,8 @@ export interface Program {
export async function createProgram(
host: CompilerHost,
options: CompilerOptions
mainFile: string,
options: CompilerOptions = {}
): Promise<Program> {
const buildCbs: any = [];
const stateMaps = new Map<Symbol, Map<any, any>>();
@ -63,7 +65,7 @@ export async function createProgram(
let error = false;
const program: Program = {
compilerOptions: options || {},
compilerOptions: options,
globalNamespace: createGlobalNamespace(),
sourceFiles: [],
literalTypes: new Map(),
@ -94,7 +96,7 @@ export async function createProgram(
await loadStandardLibrary(program);
}
await loadMain(options);
await loadMain(mainFile, options);
const checker = (program.checker = createChecker(program));
program.checker.checkProgram(program);
@ -187,36 +189,40 @@ export async function createProgram(
}
}
async function loadDirectory(rootDir: string) {
const dir = await host.readDir(rootDir);
for (const entry of dir) {
if (entry.isFile()) {
const path = join(rootDir, entry.name);
if (entry.name.endsWith(".js")) {
await loadJsFile(path);
} else if (entry.name.endsWith(".adl")) {
await loadAdlFile(path);
}
}
}
async function loadDirectory(dir: string, diagnosticTarget?: DiagnosticTarget) {
const pkgJsonPath = resolve(dir, "package.json");
let [pkg] = await loadFile(host.readFile, pkgJsonPath, JSON.parse, program.reportDiagnostic, {
allowFileNotFound: true,
diagnosticTarget,
});
const mainFile = resolve(dir, pkg?.adlMain ?? "main.adl");
await loadAdlFile(mainFile, diagnosticTarget);
}
async function loadAdlFile(path: string) {
async function loadAdlFile(path: string, diagnosticTarget?: DiagnosticTarget) {
if (seenSourceFiles.has(path)) {
return;
}
seenSourceFiles.add(path);
const contents = await host.readFile(path);
if (!contents) {
throw new Error("Couldn't load ADL file " + path);
}
const contents = await doIO(host.readFile, path, program.reportDiagnostic, {
diagnosticTarget,
});
await evalAdlScript(contents, path);
if (contents) {
await evalAdlScript(contents, path);
}
}
async function loadJsFile(path: string) {
const exports = await host.getJsImport(path);
async function loadJsFile(path: string, diagnosticTarget: DiagnosticTarget) {
const exports = await doIO(host.getJsImport, path, program.reportDiagnostic, {
diagnosticTarget,
jsDiagnosticTarget: { file: createSourceFile("", path), pos: 0, end: 0 },
});
if (!exports) {
return;
}
for (const match of Object.keys(exports)) {
// bind JS files early since this is the only work
@ -279,12 +285,11 @@ export async function createProgram(
const ext = extname(target);
if (ext === "") {
// look for a main.adl
await loadAdlFile(join(target, "main.adl"));
} else if (ext === ".js") {
await loadJsFile(target);
await loadDirectory(target, stmt);
} else if (ext === ".js" || ext === ".mjs") {
await loadJsFile(target, stmt);
} else if (ext === ".adl") {
await loadAdlFile(target);
await loadAdlFile(target, stmt);
} else {
program.reportDiagnostic(
"Import paths must reference either a directory, a .adl file, or .js file",
@ -342,7 +347,13 @@ export async function createProgram(
host
.realpath(path)
.then((p) => cb(null, p))
.catch((e) => cb(e));
.catch((e) => {
if (e.code === "ENOENT" || e.code === "ENOTDIR") {
cb(null, path);
} else {
cb(e);
}
});
},
packageFilter(pkg) {
// this lets us follow node resolve semantics more-or-less exactly
@ -355,8 +366,7 @@ export async function createProgram(
if (err) {
rejectP(err);
} else if (!resolved) {
// I don't know when this happens
rejectP(new Error("Couldn't resolve module"));
rejectP(new Error("BUG: Module resolution succeeded but didn't return a value."));
} else {
resolveP(resolved);
}
@ -365,14 +375,9 @@ export async function createProgram(
});
}
async function loadMain(options: CompilerOptions) {
if (!options.mainFile) {
throw new Error("Must specify a main file");
}
const mainPath = resolve(host.getCwd(), options.mainFile);
const mainStat = await getMainPathStats(mainPath);
async function loadMain(mainFile: string, options: CompilerOptions) {
const mainPath = resolve(host.getCwd(), mainFile);
const mainStat = await doIO(host.stat, mainPath, program.reportDiagnostic);
if (!mainStat) {
return;
}
@ -383,18 +388,6 @@ export async function createProgram(
}
}
async function getMainPathStats(mainPath: string) {
try {
return await host.stat(mainPath);
} catch (e) {
if (e.code === "ENOENT") {
program.reportDiagnostic(Message.FileNotFound, NoTarget, [mainPath]);
return undefined;
}
throw e;
}
}
function getOption(key: string): string | undefined {
return (options.miscOptions || {})[key];
}
@ -458,9 +451,9 @@ export async function createProgram(
}
export async function compile(
rootDir: string,
mainFile: string,
host: CompilerHost,
options?: CompilerOptions
): Promise<Program> {
return await createProgram(host, { mainFile: rootDir, ...options });
return await createProgram(host, mainFile, options);
}

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

@ -525,7 +525,7 @@ export interface Dirent {
export interface CompilerHost {
// read a utf-8 encoded file
readFile(path: string): Promise<string | undefined>;
readFile(path: string): Promise<string>;
// read the contents of a directory
readDir(path: string): Promise<Dirent[]>;

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

@ -2,7 +2,15 @@ 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 { CompilerHost } from "./types.js";
import {
createDiagnostic,
createSourceFile,
DiagnosticHandler,
DiagnosticTarget,
Message,
NoTarget,
} from "./diagnostics.js";
import { CompilerHost, SourceFile } from "./types.js";
export const adlVersion = getVersion();
@ -40,6 +48,73 @@ export function deepClone<T>(value: T): T {
return value;
}
export interface FileHandlingOptions {
allowFileNotFound?: boolean;
diagnosticTarget?: DiagnosticTarget;
jsDiagnosticTarget?: DiagnosticTarget;
}
export async function doIO<T>(
action: (path: string) => Promise<T>,
path: string,
reportDiagnostic: DiagnosticHandler,
options?: FileHandlingOptions
): Promise<T | undefined> {
let result;
try {
result = await action(path);
} catch (e) {
let diagnostic;
let target = options?.diagnosticTarget ?? NoTarget;
// blame the JS file, not the ADL import statement for JS syntax errors.
if (e instanceof SyntaxError && options?.jsDiagnosticTarget) {
target = options.jsDiagnosticTarget;
}
switch (e.code) {
case "ENOENT":
if (options?.allowFileNotFound) {
return undefined;
}
diagnostic = createDiagnostic(Message.FileNotFound, target, [path]);
break;
default:
diagnostic = createDiagnostic(e.message, target);
break;
}
reportDiagnostic(diagnostic);
return undefined;
}
return result;
}
export async function loadFile<T>(
read: (path: string) => Promise<string>,
path: string,
load: (contents: string) => T,
reportDiagnostic: DiagnosticHandler,
options?: FileHandlingOptions
): Promise<[T | undefined, SourceFile]> {
const contents = await doIO(read, path, reportDiagnostic, options);
if (!contents) {
return [undefined, createSourceFile("", path)];
}
const file = createSourceFile(contents, path);
let data: T;
try {
data = load(contents);
} catch (e) {
reportDiagnostic({ message: e.message, severity: "error", file });
return [undefined, file];
}
return [data, file];
}
export const NodeHost: CompilerHost = {
readFile: (path: string) => readFile(path, "utf-8"),
readDir: (path: string) => readdir(path, { withFileTypes: true }),
@ -49,7 +124,7 @@ export const NodeHost: CompilerHost = {
getJsImport: (path: string) => import(pathToFileURL(path).href),
getLibDirs() {
const rootDir = this.getExecutionRoot();
return [join(rootDir, "lib"), join(rootDir, "dist/lib")];
return [join(rootDir, "lib")];
},
stat(path: string) {
return stat(path);

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

@ -1,8 +1,8 @@
import { readFile } from "fs/promises";
import { basename, extname, join } from "path";
import { createDiagnostic, createSourceFile } from "../compiler/diagnostics.js";
import { Message } from "../compiler/diagnostics.js";
import { Diagnostic } from "../compiler/types.js";
import { deepClone, deepFreeze } from "../compiler/util.js";
import { deepClone, deepFreeze, loadFile } from "../compiler/util.js";
import { ConfigValidator } from "./config-validator.js";
import { ADLConfig } from "./types.js";
@ -24,14 +24,14 @@ const defaultConfig: ADLConfig = deepFreeze({
export async function loadADLConfigInDir(directoryPath: string): Promise<ADLConfig> {
for (const filename of configFilenames) {
const filePath = join(directoryPath, filename);
try {
return await loadADLConfigFile(filePath);
} catch (e) {
if (e.code === "ENOENT") {
continue;
}
throw e;
const config = await loadADLConfigFile(filePath);
if (
config.diagnostics.length === 1 &&
config.diagnostics[0].code === Message.FileNotFound.code
) {
continue;
}
return config;
}
return deepClone(defaultConfig);
}
@ -78,35 +78,32 @@ async function loadConfigFile(
filePath: string,
loadData: (content: string) => any
): Promise<ADLConfig> {
const content = await readFile(filePath, "utf-8");
const file = createSourceFile(content, filePath);
const diagnostics: Diagnostic[] = [];
const reportDiagnostic = (d: Diagnostic) => diagnostics.push(d);
let loadDiagnostics: Diagnostic[];
let data: any;
try {
data = loadData(content);
loadDiagnostics = [];
} catch (e) {
loadDiagnostics = [createDiagnostic(e.message, { file, pos: 0, end: 0 })];
let [data, file] = await loadFile(
(path) => readFile(path, "utf-8"),
filePath,
loadData,
reportDiagnostic
);
if (data) {
configValidator.validateConfig(data, file, reportDiagnostic);
}
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;
if (!data || diagnostics.length > 0) {
// NOTE: Don't trust the data if there are errors and use default
// config. Otherwise, we may return an object that does not conform to
// ADLConfig's typing.
data = deepClone(defaultConfig);
} else {
mergeDefaults(data, defaultConfig);
}
return {
...data,
filename: filePath,
diagnostics,
};
data.filename = filePath;
data.diagnostics = diagnostics;
return data;
}
/**

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

@ -1,5 +1,5 @@
import Ajv, { ErrorObject } from "ajv";
import { compilerAssert } from "../compiler/diagnostics.js";
import { compilerAssert, DiagnosticHandler } from "../compiler/diagnostics.js";
import { Diagnostic, SourceFile } from "../compiler/types.js";
import { ADLConfigJsonSchema } from "./config-schema.js";
import { ADLRawConfig } from "./types.js";
@ -15,20 +15,28 @@ export class ConfigValidator {
* @param file @optional file for errors tracing.
* @returns Validation
*/
public validateConfig(config: ADLRawConfig, file?: SourceFile): Diagnostic[] {
public validateConfig(
config: ADLRawConfig,
file: SourceFile,
reportDiagnostic: DiagnosticHandler
): void {
const validate = this.ajv.compile(ADLConfigJsonSchema);
const valid = validate(config);
compilerAssert(
!valid || !validate.errors,
"There should be errors reported if the config file is not valid."
);
return validate.errors?.map((e) => ajvErrorToDiagnostic(e, file)) ?? [];
for (const error of validate.errors ?? []) {
const diagnostic = ajvErrorToDiagnostic(error, file);
reportDiagnostic(diagnostic);
}
}
}
const IGNORED_AJV_PARAMS = new Set(["type", "errors"]);
function ajvErrorToDiagnostic(error: ErrorObject, file?: SourceFile): Diagnostic {
function ajvErrorToDiagnostic(error: ErrorObject, file: SourceFile): Diagnostic {
const messageLines = [`Schema violation: ${error.message} (${error.instancePath || "/"})`];
for (const [name, value] of Object.entries(error.params).filter(
([name]) => !IGNORED_AJV_PARAMS.has(name)
@ -37,9 +45,6 @@ function ajvErrorToDiagnostic(error: ErrorObject, file?: SourceFile): Diagnostic
messageLines.push(` ${name}: ${formattedValue}`);
}
return {
severity: "error",
message: messageLines.join("\n"),
...(file && { file }),
};
const message = messageLines.join("\n");
return { message, severity: "error", file };
}

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

@ -0,0 +1,2 @@
import "../dist/lib/decorators.js";
import "./lib.adl";

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

@ -11,7 +11,7 @@ describe("adl: aliases", () => {
it("can alias a union expression", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
alias Foo = int32 | string;
alias Bar = "hi" | 10;
@ -37,7 +37,7 @@ describe("adl: aliases", () => {
it("can alias a deep union expression", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
alias Foo = int32 | string;
alias Bar = "hi" | 10;
@ -65,7 +65,7 @@ describe("adl: aliases", () => {
it("can alias a union expression with parameters", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
alias Foo<T> = int32 | T;
@ -88,7 +88,7 @@ describe("adl: aliases", () => {
it("can alias a deep union expression with parameters", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
alias Foo<T> = int32 | T;
alias Bar<T, U> = Foo<T> | Foo<U>;
@ -114,7 +114,7 @@ describe("adl: aliases", () => {
it("can alias an intersection expression", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
alias Foo = {a: string} & {b: string};
alias Bar = {c: string} & {d: string};
@ -140,7 +140,7 @@ describe("adl: aliases", () => {
it("can be used like any model", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test model Test { a: string };

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

@ -10,7 +10,7 @@ describe("adl: semantic checks on source with parse errors", () => {
it("reports semantic errors in addition to parse errors", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`model M extends Q {
a: B;
a: C;

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

@ -11,7 +11,7 @@ describe("adl: duplicate declarations", () => {
it("reports duplicate template parameters", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
model A<T, T> { }
`
@ -23,7 +23,7 @@ describe("adl: duplicate declarations", () => {
it("reports duplicate model declarations in global scope", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
model A { }
model A { }
@ -36,7 +36,7 @@ describe("adl: duplicate declarations", () => {
it("reports duplicate model declarations in a single namespace", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Foo;
model A { }
@ -50,7 +50,7 @@ describe("adl: duplicate declarations", () => {
it("reports duplicate model declarations across multiple namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace N {
model A { };
@ -67,6 +67,13 @@ describe("adl: duplicate declarations", () => {
});
it("reports duplicate model declarations across multiple files and namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`

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

@ -11,7 +11,7 @@ describe("adl: enums", () => {
it("can be valueless", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test enum E {
A, B, C
@ -31,7 +31,7 @@ describe("adl: enums", () => {
it("can have values", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test enum E {
@test("A") A: "a";
@ -56,7 +56,7 @@ describe("adl: enums", () => {
it("can be a model property", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Foo;
enum E { A, B, C }

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

@ -10,7 +10,7 @@ describe("adl: loader", () => {
it("loads ADL and JS files", async () => {
testHost.addJsFile("blue.js", { blue() {} });
testHost.addAdlFile(
"a.adl",
"main.adl",
`
import "./b.adl";
import "./blue.js";
@ -39,6 +39,6 @@ describe("adl: loader", () => {
`
);
await testHost.compile("a.adl");
await testHost.compile("main.adl");
});
});

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

@ -17,8 +17,9 @@ describe("adl: namespaces with blocks", () => {
it("can be decorated", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
import "./blue.js";
@blue @test namespace Z.Q;
@blue @test namespace N { }
@blue @test namespace X.Y { }
@ -37,7 +38,7 @@ describe("adl: namespaces with blocks", () => {
it("merges like namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test
namespace N { @test model X { x: string } }
@ -58,6 +59,14 @@ describe("adl: namespaces with blocks", () => {
});
it("merges like namespaces across files", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -90,6 +99,14 @@ describe("adl: namespaces with blocks", () => {
});
it("merges sub-namespaces across files", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -117,7 +134,7 @@ describe("adl: namespaces with blocks", () => {
it("can see things in outer scope same file", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
model A { }
namespace N { model B extends A { } }
@ -127,6 +144,14 @@ describe("adl: namespaces with blocks", () => {
});
it("can see things in outer scope cross file", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -153,7 +178,7 @@ describe("adl: namespaces with blocks", () => {
it("accumulates declarations inside of it", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test namespace Foo {
namespace Bar { };
@ -163,7 +188,7 @@ describe("adl: namespaces with blocks", () => {
`
);
const { Foo } = (await testHost.compile("/a.adl")) as {
const { Foo } = (await testHost.compile("./")) as {
Foo: NamespaceType;
};
@ -187,6 +212,14 @@ describe("adl: blockless namespaces", () => {
});
it("merges properly with other namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
import "./c.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -215,7 +248,7 @@ describe("adl: blockless namespaces", () => {
it("does lookup correctly", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Repro;
model Yo {
@ -235,7 +268,7 @@ describe("adl: blockless namespaces", () => {
it("does lookup correctly with nested namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace Repro;
model Yo {
@ -265,7 +298,7 @@ describe("adl: blockless namespaces", () => {
it("binds correctly", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
namespace N.M;
model A { }
@ -287,7 +320,7 @@ describe("adl: blockless namespaces", () => {
it("works with blockful namespaces", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test
namespace N;
@ -314,6 +347,13 @@ describe("adl: blockless namespaces", () => {
});
it("works with nested blockless and blockfull namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`

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

@ -17,8 +17,9 @@ describe("adl: spread", () => {
it("clones decorated properties", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
import "./blue.js";
model A { @blue foo: string }
model B { @blue bar: string }
@test model C { ... A, ... B }

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

@ -10,6 +10,13 @@ describe("adl: using statements", () => {
});
it("works in global scope", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -33,6 +40,13 @@ describe("adl: using statements", () => {
});
it("works in namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -57,6 +71,13 @@ describe("adl: using statements", () => {
});
it("works with dotted namespaces", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -80,6 +101,13 @@ describe("adl: using statements", () => {
});
it("throws errors for duplicate imported usings", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -101,6 +129,13 @@ describe("adl: using statements", () => {
});
it("throws errors for different usings with the same bindings", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -126,6 +161,13 @@ describe("adl: using statements", () => {
});
it("resolves 'local' decls over usings", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`
@ -151,6 +193,13 @@ describe("adl: using statements", () => {
});
it("usings are local to a file", async () => {
testHost.addAdlFile(
"main.adl",
`
import "./a.adl";
import "./b.adl";
`
);
testHost.addAdlFile(
"a.adl",
`

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

@ -1,8 +1,10 @@
import { deepStrictEqual } from "assert";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { createSourceFile } from "../../compiler/diagnostics.js";
import { Diagnostic } from "../../compiler/types.js";
import { ConfigValidator } from "../../config/config-validator.js";
import { loadADLConfigInDir } from "../../config/index.js";
import { ADLRawConfig, loadADLConfigInDir } from "../../config/index.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -107,10 +109,18 @@ describe("adl: config file loading", () => {
describe("validation", () => {
const validator = new ConfigValidator();
const file = createSourceFile("<content>", "<path>");
function validate(data: ADLRawConfig) {
const diagnostics: Diagnostic[] = [];
validator.validateConfig(data, file, (d) => diagnostics.push(d));
return diagnostics;
}
it("does not allow additional properties", () => {
deepStrictEqual(validator.validateConfig({ someCustomProp: true } as any), [
deepStrictEqual(validate({ someCustomProp: true } as any), [
{
file,
severity: "error",
message:
"Schema violation: must NOT have additional properties (/)\n additionalProperty: someCustomProp",
@ -119,16 +129,17 @@ describe("adl: config file loading", () => {
});
it("fails if passing the wrong type", () => {
deepStrictEqual(validator.validateConfig({ emitters: true } as any), [
deepStrictEqual(validate({ emitters: true } as any), [
{
file,
severity: "error",
message: "Schema violation: must be object (/emitters)",
},
]);
});
it("succeeeds if config is valid", () => {
deepStrictEqual(validator.validateConfig({ lint: { rules: { foo: "on" } } }), []);
it("succeeds if config is valid", () => {
deepStrictEqual(validate({ lint: { rules: { foo: "on" } } }), []);
});
});
});

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

@ -12,7 +12,7 @@ describe("adl: range limiting decorators", () => {
it("applies @minimum and @maximum decorators", async () => {
testHost.addAdlFile(
"a.adl",
"main.adl",
`
@test model A { @minValue(15) foo: int32; @maxValue(55) boo: float32; }
@test model B { @maxValue(20) bar: int64; @minValue(23) car: float64; }

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

@ -12,10 +12,7 @@ describe("adl: libraries", () => {
const mainFile = fileURLToPath(
new URL(`../../../test/libraries/${lib}/main.adl`, import.meta.url)
);
await createProgram(NodeHost, {
mainFile,
noEmit: true,
});
await createProgram(NodeHost, mainFile, { noEmit: true });
} catch (e) {
console.error(e.diagnostics);
throw e;

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

@ -1,5 +1,5 @@
import { readdir, readFile } from "fs/promises";
import { basename, isAbsolute, join, normalize, relative, resolve, sep } from "path";
import { basename, extname, isAbsolute, relative, resolve, sep } from "path";
import { fileURLToPath, pathToFileURL } from "url";
import { formatDiagnostic, logDiagnostics, logVerboseTestOutput } from "../compiler/diagnostics.js";
import { CompilerOptions } from "../compiler/options.js";
@ -22,23 +22,33 @@ export interface TestHost {
/**
* Virtual filesystem used in the tests.
*/
fs: { [name: string]: string };
fs: Map<string, string>;
}
class TestHostError extends Error {
constructor(message: string, public code: "ENOENT" | "ERR_MODULE_NOT_FOUND") {
super(message);
}
}
export async function createTestHost(): Promise<TestHost> {
const testTypes: Record<string, Type> = {};
let program: Program = undefined as any; // in practice it will always be initialized
const virtualFs: { [name: string]: string } = {};
const jsImports: { [path: string]: Promise<any> } = {};
const virtualFs = new Map<string, string>();
const jsImports = new Map<string, Promise<any>>();
const compilerHost: CompilerHost = {
async readFile(path: string) {
return virtualFs[path];
const contents = virtualFs.get(path);
if (contents === undefined) {
throw new TestHostError(`File ${path} not found.`, "ENOENT");
}
return contents;
},
async readDir(path: string) {
const contents = [];
for (const fsPath of Object.keys(virtualFs)) {
for (const fsPath of virtualFs.keys()) {
if (isContainedIn(path, fsPath)) {
contents.push({
isFile() {
@ -56,7 +66,7 @@ export async function createTestHost(): Promise<TestHost> {
},
async writeFile(path: string, content: string) {
virtualFs[path] = content;
virtualFs.set(path, content);
},
getLibDirs() {
@ -68,7 +78,11 @@ export async function createTestHost(): Promise<TestHost> {
},
getJsImport(path) {
return jsImports[path];
const module = jsImports.get(path);
if (module === undefined) {
throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND");
}
return module;
},
getCwd() {
@ -76,7 +90,7 @@ export async function createTestHost(): Promise<TestHost> {
},
async stat(path: string) {
if (virtualFs.hasOwnProperty(path)) {
if (virtualFs.has(path)) {
return {
isDirectory() {
return false;
@ -87,7 +101,7 @@ export async function createTestHost(): Promise<TestHost> {
};
}
for (const fsPath of Object.keys(virtualFs)) {
for (const fsPath of virtualFs.keys()) {
if (fsPath.startsWith(path) && fsPath !== path) {
return {
isDirectory() {
@ -100,7 +114,7 @@ export async function createTestHost(): Promise<TestHost> {
}
}
throw { code: "ENOENT" };
throw new TestHostError(`File ${path} not found`, "ENOENT");
},
// symlinks not supported in test-host
@ -110,27 +124,33 @@ export async function createTestHost(): Promise<TestHost> {
};
// load standard library into the vfs
for (const relDir of ["../../lib", "../../../lib"]) {
for (const [relDir, virtualDir] of [
["../../lib", "/.adl/dist/lib"],
["../../../lib", "/.adl/lib"],
]) {
const dir = resolve(fileURLToPath(import.meta.url), relDir);
const contents = await readdir(dir, { withFileTypes: true });
for (const entry of contents) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const realPath = resolve(dir, entry.name);
const virtualPath = resolve(virtualDir, entry.name);
if (entry.isFile()) {
const path = join(dir, entry.name);
const virtualDir = compilerHost.getLibDirs()[0];
const key = normalize(join(virtualDir, entry.name));
if (entry.name.endsWith(".js")) {
jsImports[key] = import(pathToFileURL(path).href);
virtualFs[key] = ""; // don't need contents.
} else {
const contents = await readFile(path, "utf-8");
virtualFs[key] = contents;
switch (extname(entry.name)) {
case ".adl":
const contents = await readFile(realPath, "utf-8");
virtualFs.set(virtualPath, contents);
break;
case ".js":
case ".mjs":
jsImports.set(virtualPath, import(pathToFileURL(realPath).href));
virtualFs.set(virtualPath, ""); // don't need contents.
break;
}
}
}
}
// add test decorators
addAdlFile("/.adl/test-lib/main.adl", 'import "./test.js";');
addJsFile("/.adl/test-lib/test.js", {
test(_: any, target: Type, name?: string) {
if (!name) {
@ -161,26 +181,25 @@ export async function createTestHost(): Promise<TestHost> {
};
function addAdlFile(path: string, contents: string) {
virtualFs[resolve(compilerHost.getCwd(), path)] = contents;
virtualFs.set(resolve(compilerHost.getCwd(), path), contents);
}
function addJsFile(path: string, contents: any) {
const key = resolve(compilerHost.getCwd(), path);
// don't need file contents;
virtualFs[key] = "";
jsImports[key] = new Promise((r) => r(contents));
virtualFs.set(key, ""); // don't need contents
jsImports.set(key, new Promise((r) => r(contents)));
}
async function addRealAdlFile(path: string, existingPath: string) {
virtualFs[resolve(compilerHost.getCwd(), path)] = await readFile(existingPath, "utf8");
virtualFs.set(resolve(compilerHost.getCwd(), path), await readFile(existingPath, "utf8"));
}
async function addRealJsFile(path: string, existingPath: string) {
const key = resolve(compilerHost.getCwd(), path);
const exports = await import(pathToFileURL(existingPath).href);
virtualFs[key] = "";
jsImports[key] = exports;
virtualFs.set(key, "");
jsImports.set(key, exports);
}
async function compile(main: string, options: CompilerOptions = {}) {
@ -198,18 +217,15 @@ export async function createTestHost(): Promise<TestHost> {
}
async function compileAndDiagnose(
main: string,
mainFile: string,
options: CompilerOptions = {}
): Promise<[Record<string, Type>, readonly Diagnostic[]]> {
// default is noEmit
if (!options.hasOwnProperty("noEmit")) {
options.noEmit = true;
if (options.noEmit === undefined) {
// default for tests is noEmit
options = { ...options, noEmit: true };
}
program = await createProgram(compilerHost, {
mainFile: main,
...options,
});
program = await createProgram(compilerHost, mainFile, options);
logVerboseTestOutput((log) => logDiagnostics(program.diagnostics, log));
return [testTypes, program.diagnostics];
}