Implement import statements (#397)
This commit is contained in:
Родитель
5f8b5c3ca9
Коммит
3d5410354f
|
@ -7,7 +7,7 @@ import { spawnSync } from "child_process";
|
|||
import { CompilerOptions } from "../compiler/options.js";
|
||||
import { DiagnosticError, dumpError, logDiagnostics } from "./diagnostics.js";
|
||||
import { adlVersion } from "./util.js";
|
||||
import { readFile, mkdtemp, readdir, rmdir } from "fs/promises";
|
||||
import { stat, readFile, mkdtemp, readdir, rmdir } from "fs/promises";
|
||||
import os from "os";
|
||||
import { CompilerHost } from "./types.js";
|
||||
|
||||
|
@ -83,6 +83,9 @@ const NodeHost: CompilerHost = {
|
|||
const rootDir = this.getExecutionRoot();
|
||||
return [join(rootDir, "lib"), join(rootDir, "dist/lib")];
|
||||
},
|
||||
stat(path: string) {
|
||||
return stat(path);
|
||||
},
|
||||
};
|
||||
|
||||
async function compileInput(compilerOptions: CompilerOptions) {
|
||||
|
|
|
@ -37,6 +37,10 @@ export function parse(code: string | Types.SourceFile) {
|
|||
throw error("Blockless namespaces can't follow other declarations");
|
||||
}
|
||||
seenBlocklessNs = true;
|
||||
} else if (item.kind === Types.SyntaxKind.ImportStatement) {
|
||||
if (seenDecl || seenBlocklessNs) {
|
||||
throw error("Imports must come prior to namespaces or other declarations");
|
||||
}
|
||||
} else {
|
||||
seenDecl = true;
|
||||
}
|
||||
|
@ -499,48 +503,18 @@ export function parse(code: string | Types.SourceFile) {
|
|||
const pos = tokenPos();
|
||||
|
||||
parseExpected(Token.ImportKeyword);
|
||||
const id = parseIdentifier();
|
||||
let as: Array<Types.NamedImportNode> = [];
|
||||
|
||||
if (token() === Token.Identifier && tokenValue() === "as") {
|
||||
parseExpected(Token.Identifier);
|
||||
parseExpected(Token.OpenBrace);
|
||||
|
||||
if (token() !== Token.CloseBrace) {
|
||||
as = parseNamedImports();
|
||||
}
|
||||
|
||||
parseExpected(Token.CloseBrace);
|
||||
}
|
||||
|
||||
const pathLiteral = parseStringLiteral();
|
||||
const path = pathLiteral.value;
|
||||
parseExpected(Token.Semicolon);
|
||||
return finishNode(
|
||||
{
|
||||
kind: Types.SyntaxKind.ImportStatement,
|
||||
as,
|
||||
id,
|
||||
path,
|
||||
},
|
||||
pos
|
||||
);
|
||||
}
|
||||
|
||||
function parseNamedImports(): Array<Types.NamedImportNode> {
|
||||
const names: Array<Types.NamedImportNode> = [];
|
||||
do {
|
||||
const pos = tokenPos();
|
||||
names.push(
|
||||
finishNode(
|
||||
{
|
||||
kind: Types.SyntaxKind.NamedImport,
|
||||
id: parseIdentifier(),
|
||||
},
|
||||
pos
|
||||
)
|
||||
);
|
||||
} while (parseOptional(Token.Comma));
|
||||
return names;
|
||||
}
|
||||
|
||||
function parseDecoratorExpression(): Types.DecoratorExpressionNode {
|
||||
const pos = tokenPos();
|
||||
parseExpected(Token.At);
|
||||
|
@ -824,7 +798,7 @@ export function visitChildren<T>(node: Types.Node, cb: NodeCb<T>): T | undefined
|
|||
case Types.SyntaxKind.DecoratorExpression:
|
||||
return visitNode(cb, node.target) || visitEach(cb, node.arguments);
|
||||
case Types.SyntaxKind.ImportStatement:
|
||||
return visitNode(cb, node.id) || visitEach(cb, node.as);
|
||||
return;
|
||||
case Types.SyntaxKind.OperationStatement:
|
||||
return (
|
||||
visitEach(cb, node.decorators) ||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { resolve } from "path";
|
||||
import { dirname, extname, isAbsolute, resolve } from "path";
|
||||
import { lstat } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { createBinder, SymbolTable } from "./binder.js";
|
||||
|
@ -15,7 +15,6 @@ import {
|
|||
ModelType,
|
||||
SyntaxKind,
|
||||
Type,
|
||||
SourceFile,
|
||||
DecoratorSymbol,
|
||||
CompilerHost,
|
||||
NamespaceStatementNode,
|
||||
|
@ -42,6 +41,7 @@ export async function createProgram(
|
|||
): Promise<Program> {
|
||||
const buildCbs: any = [];
|
||||
|
||||
const seenSourceFiles = new Set<string>();
|
||||
const program: Program = {
|
||||
compilerOptions: options || {},
|
||||
globalNamespace: createGlobalNamespace(),
|
||||
|
@ -149,33 +149,39 @@ export async function createProgram(
|
|||
|
||||
async function loadStandardLibrary(program: Program) {
|
||||
for (const dir of host.getLibDirs()) {
|
||||
await loadDirectory(program, dir);
|
||||
await loadDirectory(dir);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDirectory(program: Program, rootDir: string) {
|
||||
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(program, path);
|
||||
await loadJsFile(path);
|
||||
} else if (entry.name.endsWith(".adl")) {
|
||||
await loadAdlFile(program, path);
|
||||
await loadAdlFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdlFile(program: Program, path: string) {
|
||||
async function loadAdlFile(path: string) {
|
||||
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);
|
||||
}
|
||||
program.evalAdlScript(contents, path);
|
||||
|
||||
await evalAdlScript(contents, path);
|
||||
}
|
||||
|
||||
async function loadJsFile(program: Program, path: string) {
|
||||
async function loadJsFile(path: string) {
|
||||
const exports = await host.getJsImport(path);
|
||||
|
||||
for (const match of Object.keys(exports)) {
|
||||
|
@ -199,13 +205,45 @@ export async function createProgram(
|
|||
// Evaluates an arbitrary line of ADL in the context of a
|
||||
// specified file path. If no path is specified, use a
|
||||
// virtual file path
|
||||
function evalAdlScript(adlScript: string, filePath?: string): void {
|
||||
async function evalAdlScript(adlScript: string, filePath?: string): Promise<void> {
|
||||
filePath = filePath ?? `__virtual_file_${++virtualFileCount}`;
|
||||
const unparsedFile = createSourceFile(adlScript, filePath);
|
||||
const sourceFile = parse(unparsedFile);
|
||||
|
||||
program.sourceFiles.push(sourceFile);
|
||||
binder.bindSourceFile(program, sourceFile);
|
||||
await evalImports(sourceFile);
|
||||
}
|
||||
|
||||
async function evalImports(file: ADLScriptNode) {
|
||||
// collect imports
|
||||
for (const stmt of file.statements) {
|
||||
if (stmt.kind !== SyntaxKind.ImportStatement) break;
|
||||
const path = stmt.path;
|
||||
const ext = extname(path);
|
||||
|
||||
let target: string;
|
||||
if (path.startsWith("./") || path.startsWith("../")) {
|
||||
target = resolve(dirname(file.file.path), path);
|
||||
} else if (isAbsolute(path)) {
|
||||
target = path;
|
||||
} else {
|
||||
throwDiagnostic("Import paths must begin with ./ or ../ or be absolute", stmt);
|
||||
}
|
||||
|
||||
if (ext === "") {
|
||||
// look for a main.adl
|
||||
await loadAdlFile(join(target, "main.adl"));
|
||||
} else if (ext === ".js") {
|
||||
await loadJsFile(target);
|
||||
} else if (ext === ".adl") {
|
||||
await loadAdlFile(target);
|
||||
} else {
|
||||
throwDiagnostic(
|
||||
"Import paths must reference either a directory, a .adl file, or .js file",
|
||||
stmt
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMain(options: CompilerOptions) {
|
||||
|
@ -215,12 +253,12 @@ export async function createProgram(
|
|||
|
||||
const mainPath = resolve(host.getCwd(), options.mainFile);
|
||||
|
||||
const mainStat = await lstat(mainPath);
|
||||
const mainStat = await host.stat(mainPath);
|
||||
|
||||
if (mainStat.isDirectory()) {
|
||||
await loadDirectory(program, mainPath);
|
||||
await loadDirectory(mainPath);
|
||||
} else {
|
||||
await loadAdlFile(program, mainPath);
|
||||
await loadAdlFile(mainPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,8 +219,7 @@ export type ScopeNode = NamespaceStatementNode | ModelStatementNode | ADLScriptN
|
|||
|
||||
export interface ImportStatementNode extends BaseNode {
|
||||
kind: SyntaxKind.ImportStatement;
|
||||
id: IdentifierNode;
|
||||
as: Array<NamedImportNode>;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface IdentifierNode extends BaseNode {
|
||||
|
@ -456,4 +455,7 @@ export interface CompilerHost {
|
|||
|
||||
// get the current working directory
|
||||
getCwd(): string;
|
||||
|
||||
// get info about a path (presently just isDirectory())
|
||||
stat(path: string): Promise<{ isDirectory(): boolean }>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { strictEqual, ok } from "assert";
|
||||
import { ModelType, NamespaceType, Type } from "../../compiler/types.js";
|
||||
import { createTestHost, TestHost } from "../test-host.js";
|
||||
|
||||
describe("loader", () => {
|
||||
let testHost: TestHost;
|
||||
|
||||
beforeEach(async () => {
|
||||
testHost = await createTestHost();
|
||||
});
|
||||
|
||||
it.only("loads ADL and JS files", async () => {
|
||||
testHost.addJsFile("blue.js", { blue() {} });
|
||||
testHost.addAdlFile(
|
||||
"a.adl",
|
||||
`
|
||||
import "./b.adl";
|
||||
import "./blue.js";
|
||||
|
||||
@blue
|
||||
model A extends B, C { }
|
||||
`
|
||||
);
|
||||
testHost.addAdlFile(
|
||||
"b.adl",
|
||||
`
|
||||
import "./test";
|
||||
model B { }
|
||||
`
|
||||
);
|
||||
testHost.addAdlFile(
|
||||
"test/main.adl",
|
||||
`
|
||||
import "./c.adl";
|
||||
`
|
||||
);
|
||||
testHost.addAdlFile(
|
||||
"test/c.adl",
|
||||
`
|
||||
model C { }
|
||||
`
|
||||
);
|
||||
|
||||
await testHost.compile("a.adl");
|
||||
});
|
||||
});
|
|
@ -57,6 +57,24 @@ export async function createTestHost(): Promise<TestHost> {
|
|||
getCwd() {
|
||||
return "/";
|
||||
},
|
||||
|
||||
async stat(path: string) {
|
||||
for (const fsPath of Object.keys(virtualFs)) {
|
||||
if (fsPath.startsWith(path) && fsPath !== path) {
|
||||
return {
|
||||
isDirectory() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDirectory() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// load standard library into the vfs
|
||||
|
|
|
@ -4,11 +4,12 @@ import { SyntaxKind } from "../compiler/types.js";
|
|||
|
||||
describe("syntax", () => {
|
||||
describe("import statements", () => {
|
||||
parseEach([
|
||||
"import x;",
|
||||
"import x as { one };",
|
||||
"import x as {};",
|
||||
"import x as { one, two };",
|
||||
parseEach(['import "x";']);
|
||||
|
||||
parseErrorEach([
|
||||
'namespace Foo { import "x"; }',
|
||||
'namespace Foo { } import "x";',
|
||||
'model Foo { } import "x";',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче