Skip to content

Commit 80ac673

Browse files
committed
Add evaluator tests
1 parent 0bf051b commit 80ac673

File tree

4 files changed

+225
-83
lines changed

4 files changed

+225
-83
lines changed

src/compiler/transformers/module/module.ts

+42-48
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
/*@internal*/
22
namespace ts {
33

4-
const enum ImportOrExportBindingReferenceKind {
5-
None,
6-
ImportedHelper,
7-
TopLevelExportBinding,
8-
ImportClause,
9-
ImportSpecifier,
10-
}
11-
12-
type ImportOrExportBindingReferenceResult =
13-
| { kind: ImportOrExportBindingReferenceKind.None, node: undefined }
14-
| { kind: ImportOrExportBindingReferenceKind.ImportedHelper, node: Identifier }
15-
| { kind: ImportOrExportBindingReferenceKind.TopLevelExportBinding, node: undefined }
16-
| { kind: ImportOrExportBindingReferenceKind.ImportClause, node: ImportClause }
17-
| { kind: ImportOrExportBindingReferenceKind.ImportSpecifier, node: ImportSpecifier };
18-
19-
const noReferenceResult: ImportOrExportBindingReferenceResult = { kind: ImportOrExportBindingReferenceKind.None, node: undefined };
20-
const topLevelExportReferenceResult: ImportOrExportBindingReferenceResult = { kind: ImportOrExportBindingReferenceKind.TopLevelExportBinding, node: undefined };
21-
224
export function transformModule(context: TransformationContext) {
235
interface AsynchronousDependencies {
246
aliasedModuleNames: Expression[];
@@ -67,7 +49,7 @@ namespace ts {
6749
let currentModuleInfo: ExternalModuleInfo; // The ExternalModuleInfo for the current file.
6850
let noSubstitution: boolean[]; // Set of nodes for which substitution rules should be ignored.
6951
let needUMDDynamicImportHelper: boolean;
70-
let bindingReferenceCache: ESMap<Node, ImportOrExportBindingReferenceResult> | undefined;
52+
let bindingReferenceCache: ESMap<Node, Identifier | SourceFile | ImportClause | ImportSpecifier | undefined> | undefined;
7153

7254
return chainBundle(context, transformSourceFile);
7355

@@ -1776,49 +1758,61 @@ namespace ts {
17761758
return node;
17771759
}
17781760

1779-
function getImportOrExportBindingReferenceWorker(node: Identifier): ImportOrExportBindingReferenceResult {
1761+
/**
1762+
* For an Identifier, gets the import or export binding that it references.
1763+
* @returns One of the following:
1764+
* - An `Identifier` if node references an external helpers module (i.e., `tslib`).
1765+
* - A `SourceFile` if the node references an export in the file.
1766+
* - An `ImportClause` or `ImportSpecifier` if the node references an import binding.
1767+
* - Otherwise, `undefined`.
1768+
*/
1769+
function getImportOrExportBindingReferenceWorker(node: Identifier): Identifier | SourceFile | ImportClause | ImportSpecifier | undefined {
17801770
if (getEmitFlags(node) & EmitFlags.HelperName) {
17811771
const externalHelpersModuleName = getExternalHelpersModuleName(currentSourceFile);
17821772
if (externalHelpersModuleName) {
1783-
return { kind: ImportOrExportBindingReferenceKind.ImportedHelper, node: externalHelpersModuleName };
1773+
return externalHelpersModuleName;
17841774
}
17851775
}
17861776
else if (!(isGeneratedIdentifier(node) && !(node.autoGenerateFlags & GeneratedIdentifierFlags.AllowNameSubstitution)) && !isLocalName(node)) {
17871777
const exportContainer = resolver.getReferencedExportContainer(node, isExportName(node));
17881778
if (exportContainer?.kind === SyntaxKind.SourceFile) {
1789-
return topLevelExportReferenceResult;
1779+
return exportContainer;
17901780
}
17911781
const importDeclaration = resolver.getReferencedImportDeclaration(node);
1792-
if (importDeclaration) {
1793-
if (isImportClause(importDeclaration)) return { kind: ImportOrExportBindingReferenceKind.ImportClause, node: importDeclaration };
1794-
if (isImportSpecifier(importDeclaration)) return { kind: ImportOrExportBindingReferenceKind.ImportSpecifier, node: importDeclaration };
1782+
if (importDeclaration && (isImportClause(importDeclaration) || isImportSpecifier(importDeclaration))) {
1783+
return importDeclaration;
17951784
}
17961785
}
1797-
return noReferenceResult;
1786+
return undefined;
17981787
}
17991788

1800-
function getImportOrExportBindingReference(node: Identifier, removeEntry: boolean): ImportOrExportBindingReferenceResult {
1801-
bindingReferenceCache ||= new Map();
1802-
let result = bindingReferenceCache.get(node);
1803-
if (!result) {
1789+
/**
1790+
* For an Identifier, gets the import or export binding that it references.
1791+
* @param removeEntry When `false`, the result is cached to avoid recomputing the result in a later substitution.
1792+
* When `true`, any cached result for the node is removed.
1793+
* @returns One of the following:
1794+
* - An `Identifier` if node references an external helpers module (i.e., `tslib`).
1795+
* - A `SourceFile` if the node references an export in the file.
1796+
* - An `ImportClause` or `ImportSpecifier` if the node references an import binding.
1797+
* - Otherwise, `undefined`.
1798+
*/
1799+
function getImportOrExportBindingReference(node: Identifier, removeEntry: boolean): Identifier | SourceFile | ImportClause | ImportSpecifier | undefined {
1800+
let result = bindingReferenceCache?.get(node);
1801+
if (!result && !bindingReferenceCache?.has(node)) {
18041802
result = getImportOrExportBindingReferenceWorker(node);
18051803
if (!removeEntry) {
1806-
switch (result.kind) {
1807-
case ImportOrExportBindingReferenceKind.ImportedHelper:
1808-
case ImportOrExportBindingReferenceKind.ImportClause:
1809-
case ImportOrExportBindingReferenceKind.ImportSpecifier:
1810-
bindingReferenceCache.set(node, result);
1811-
}
1804+
bindingReferenceCache ||= new Map();
1805+
bindingReferenceCache.set(node, result);
18121806
}
18131807
}
18141808
else if (removeEntry) {
1815-
bindingReferenceCache.delete(node);
1809+
bindingReferenceCache?.delete(node);
18161810
}
18171811
return result;
18181812
}
18191813

18201814
function substituteCallExpression(node: CallExpression) {
1821-
if (isIdentifier(node.expression) && getImportOrExportBindingReference(node.expression, /*removeEntry*/ false).kind !== ImportOrExportBindingReferenceKind.None) {
1815+
if (isIdentifier(node.expression) && getImportOrExportBindingReference(node.expression, /*removeEntry*/ false)) {
18221816
return isCallChain(node) ?
18231817
factory.updateCallChain(node,
18241818
setTextRange(factory.createComma(factory.createNumericLiteral(0), node.expression), node.expression),
@@ -1834,7 +1828,7 @@ namespace ts {
18341828
}
18351829

18361830
function substituteTaggedTemplateExpression(node: TaggedTemplateExpression) {
1837-
if (isIdentifier(node.tag) && getImportOrExportBindingReference(node.tag, /*removeEntry*/ false).kind !== ImportOrExportBindingReferenceKind.None) {
1831+
if (isIdentifier(node.tag) && getImportOrExportBindingReference(node.tag, /*removeEntry*/ false)) {
18381832
return factory.updateTaggedTemplateExpression(
18391833
node,
18401834
setTextRange(factory.createComma(factory.createNumericLiteral(0), node.tag), node.tag),
@@ -1852,30 +1846,30 @@ namespace ts {
18521846
*/
18531847
function substituteExpressionIdentifier(node: Identifier): Expression {
18541848
const result = getImportOrExportBindingReference(node, /*removeEntry*/ true);
1855-
switch (result.kind) {
1856-
case ImportOrExportBindingReferenceKind.ImportedHelper:
1857-
return factory.createPropertyAccessExpression(result.node, node);
1858-
case ImportOrExportBindingReferenceKind.TopLevelExportBinding:
1849+
switch (result?.kind) {
1850+
case SyntaxKind.Identifier: // tslib import
1851+
return factory.createPropertyAccessExpression(result, node);
1852+
case SyntaxKind.SourceFile: // top-level export
18591853
return setTextRange(
18601854
factory.createPropertyAccessExpression(
18611855
factory.createIdentifier("exports"),
18621856
factory.cloneNode(node)
18631857
),
18641858
/*location*/ node
18651859
);
1866-
case ImportOrExportBindingReferenceKind.ImportClause:
1860+
case SyntaxKind.ImportClause:
18671861
return setTextRange(
18681862
factory.createPropertyAccessExpression(
1869-
factory.getGeneratedNameForNode(result.node.parent),
1863+
factory.getGeneratedNameForNode(result.parent),
18701864
factory.createIdentifier("default")
18711865
),
18721866
/*location*/ node
18731867
);
1874-
case ImportOrExportBindingReferenceKind.ImportSpecifier:
1875-
const name = result.node.propertyName || result.node.name;
1868+
case SyntaxKind.ImportSpecifier:
1869+
const name = result.propertyName || result.name;
18761870
return setTextRange(
18771871
factory.createPropertyAccessExpression(
1878-
factory.getGeneratedNameForNode(result.node.parent?.parent?.parent || result.node),
1872+
factory.getGeneratedNameForNode(result.parent?.parent?.parent || result),
18791873
factory.cloneNode(name)
18801874
),
18811875
/*location*/ node

src/harness/evaluatorImpl.ts

+98-35
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,6 @@ namespace evaluator {
44
const sourceFile = vpath.combine(vfs.srcFolder, "source.ts");
55
const sourceFileJs = vpath.combine(vfs.srcFolder, "source.js");
66

7-
function compile(sourceText: string, options?: ts.CompilerOptions) {
8-
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
9-
fs.writeFileSync(sourceFile, sourceText);
10-
const compilerOptions: ts.CompilerOptions = {
11-
target: ts.ScriptTarget.ES5,
12-
module: ts.ModuleKind.CommonJS,
13-
lib: ["lib.esnext.d.ts", "lib.dom.d.ts"],
14-
...options
15-
};
16-
const host = new fakes.CompilerHost(fs, compilerOptions);
17-
return compiler.compileFiles(host, [sourceFile], compilerOptions);
18-
}
19-
20-
function noRequire(id: string) {
21-
throw new Error(`Module '${id}' could not be found.`);
22-
}
23-
247
// Define a custom "Symbol" constructor to attach missing built-in symbols without
258
// modifying the global "Symbol" constructor
269
const FakeSymbol: SymbolConstructor = ((description?: string) => Symbol(description)) as any;
@@ -32,8 +15,17 @@ namespace evaluator {
3215
// Add "asyncIterator" if missing
3316
if (!ts.hasProperty(FakeSymbol, "asyncIterator")) Object.defineProperty(FakeSymbol, "asyncIterator", { value: Symbol.for("Symbol.asyncIterator"), configurable: true });
3417

35-
export function evaluateTypeScript(sourceText: string, options?: ts.CompilerOptions, globals?: Record<string, any>) {
36-
const result = compile(sourceText, options);
18+
export function evaluateTypeScript(source: string | { files: vfs.FileSet, rootFiles: string[], main: string }, options?: ts.CompilerOptions, globals?: Record<string, any>) {
19+
if (typeof source === "string") source = { files: { [sourceFile]: source }, rootFiles: [sourceFile], main: sourceFile };
20+
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { files: source.files });
21+
const compilerOptions: ts.CompilerOptions = {
22+
target: ts.ScriptTarget.ES5,
23+
module: ts.ModuleKind.CommonJS,
24+
lib: ["lib.esnext.d.ts", "lib.dom.d.ts"],
25+
...options
26+
};
27+
const host = new fakes.CompilerHost(fs, compilerOptions);
28+
const result = compiler.compileFiles(host, source.rootFiles, compilerOptions);
3729
if (ts.some(result.diagnostics)) {
3830
assert.ok(/*value*/ false, "Syntax error in evaluation source text:\n" + ts.formatDiagnostics(result.diagnostics, {
3931
getCanonicalFileName: file => file,
@@ -42,29 +34,100 @@ namespace evaluator {
4234
}));
4335
}
4436

45-
const output = result.getOutput(sourceFile, "js")!;
37+
const output = result.getOutput(source.main, "js")!;
4638
assert.isDefined(output);
4739

48-
return evaluateJavaScript(output.text, globals, output.file);
40+
globals = { Symbol: FakeSymbol, ...globals };
41+
return createLoader(fs, globals)(output.file);
4942
}
5043

51-
export function evaluateJavaScript(sourceText: string, globals?: Record<string, any>, sourceFile = sourceFileJs) {
52-
globals = { Symbol: FakeSymbol, ...globals };
44+
function createLoader(fs: vfs.FileSystem, globals: Record<string, any>) {
45+
interface Module {
46+
exports: any;
47+
}
5348

54-
const globalNames: string[] = [];
55-
const globalArgs: any[] = [];
56-
for (const name in globals) {
57-
if (ts.hasProperty(globals, name)) {
58-
globalNames.push(name);
59-
globalArgs.push(globals[name]);
49+
const moduleCache = new ts.Map<string, Module>();
50+
return load;
51+
52+
function evaluate(text: string, file: string, module: Module) {
53+
const globalNames: string[] = [];
54+
const globalArgs: any[] = [];
55+
for (const name in globals) {
56+
if (ts.hasProperty(globals, name)) {
57+
globalNames.push(name);
58+
globalArgs.push(globals[name]);
59+
}
60+
}
61+
const base = vpath.dirname(file);
62+
const localRequire = (id: string) => requireModule(id, base);
63+
const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${text} })`;
64+
// eslint-disable-next-line no-eval
65+
const evaluateThunk = (void 0, eval)(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void;
66+
evaluateThunk.call(globals, module, module.exports, localRequire, vpath.dirname(file), file, FakeSymbol, ...globalArgs);
67+
}
68+
69+
function loadModule(file: string): Module {
70+
if (!ts.isExternalModuleNameRelative(file)) throw new Error(`Module '${file}' could not be found.`);
71+
let module = moduleCache.get(file);
72+
if (module) return module;
73+
moduleCache.set(file, module = { exports: {} });
74+
try {
75+
const sourceText = fs.readFileSync(file, "utf8");
76+
evaluate(sourceText, file, module);
77+
return module;
78+
}
79+
catch (e) {
80+
moduleCache.delete(file);
81+
throw e;
6082
}
6183
}
6284

63-
const evaluateText = `(function (module, exports, require, __dirname, __filename, ${globalNames.join(", ")}) { ${sourceText} })`;
64-
// eslint-disable-next-line no-eval
65-
const evaluateThunk = (void 0, eval)(evaluateText) as (module: any, exports: any, require: (id: string) => any, dirname: string, filename: string, ...globalArgs: any[]) => void;
66-
const module: { exports: any; } = { exports: {} };
67-
evaluateThunk.call(globals, module, module.exports, noRequire, vpath.dirname(sourceFile), sourceFile, FakeSymbol, ...globalArgs);
68-
return module.exports;
85+
function isFile(file: string) {
86+
return fs.existsSync(file) && fs.statSync(file).isFile();
87+
}
88+
89+
function loadAsFile(file: string): Module | undefined {
90+
if (isFile(file)) return loadModule(file);
91+
if (isFile(file + ".js")) return loadModule(file + ".js");
92+
return undefined;
93+
}
94+
95+
function loadIndex(dir: string): Module | undefined {
96+
const indexFile = vpath.resolve(dir, "index.js");
97+
if (isFile(indexFile)) return loadModule(indexFile);
98+
return undefined;
99+
}
100+
101+
function loadAsDirectory(dir: string): Module | undefined {
102+
const packageFile = vpath.resolve(dir, "package.json");
103+
if (isFile(packageFile)) {
104+
const text = fs.readFileSync(packageFile, "utf8");
105+
const json = JSON.parse(text);
106+
if (json.main) {
107+
const main = vpath.resolve(dir, json.main);
108+
const result = loadAsFile(main) || loadIndex(main);
109+
if (result === undefined) throw new Error("Module not found");
110+
}
111+
}
112+
return loadIndex(dir);
113+
}
114+
115+
function requireModule(id: string, base: string) {
116+
if (!ts.isExternalModuleNameRelative(id)) throw new Error(`Module '${id}' could not be found.`);
117+
const file = vpath.resolve(base, id);
118+
const module = loadAsFile(file) || loadAsDirectory(file);
119+
if (!module) throw new Error(`Module '${id}' could not be found.`);
120+
return module.exports;
121+
}
122+
123+
function load(file: string) {
124+
return requireModule(file, fs.cwd());
125+
}
126+
}
127+
128+
export function evaluateJavaScript(sourceText: string, globals?: Record<string, any>, sourceFile = sourceFileJs) {
129+
globals = { Symbol: FakeSymbol, ...globals };
130+
const fs = new vfs.FileSystem(/*ignoreCase*/ false, { files: { [sourceFile]: sourceText } });
131+
return createLoader(fs, globals)(sourceFile);
69132
}
70133
}

src/testRunner/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"unittests/evaluation/asyncGenerator.ts",
9292
"unittests/evaluation/awaiter.ts",
9393
"unittests/evaluation/destructuring.ts",
94+
"unittests/evaluation/externalModules.ts",
9495
"unittests/evaluation/forAwaitOf.ts",
9596
"unittests/evaluation/forOf.ts",
9697
"unittests/evaluation/optionalCall.ts",

0 commit comments

Comments
 (0)