Skip to content

Commit 7751ecb

Browse files
ajafffrbuckton
andauthored
fix receiver on calls of imported and exported functions (#35877)
* fix receiver of imported and exported functions fixes: #35420 * Rebase against master and clean up substitution flow * Add evaluator tests * Fix evaluator tests Co-authored-by: Ron Buckton <[email protected]>
1 parent a8742e3 commit 7751ecb

File tree

166 files changed

+595
-372
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

166 files changed

+595
-372
lines changed

src/compiler/transformers/module/module.ts

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*@internal*/
22
namespace ts {
3+
34
export function transformModule(context: TransformationContext) {
45
interface AsynchronousDependencies {
56
aliasedModuleNames: Expression[];
@@ -34,6 +35,8 @@ namespace ts {
3435
context.onEmitNode = onEmitNode;
3536
context.enableSubstitution(SyntaxKind.Identifier); // Substitutes expression identifiers with imported/exported symbols.
3637
context.enableSubstitution(SyntaxKind.BinaryExpression); // Substitutes assignments to exported symbols.
38+
context.enableSubstitution(SyntaxKind.CallExpression); // Substitutes expression identifiers with imported/exported symbols.
39+
context.enableSubstitution(SyntaxKind.TaggedTemplateExpression); // Substitutes expression identifiers with imported/exported symbols.
3740
context.enableSubstitution(SyntaxKind.PrefixUnaryExpression); // Substitutes updates to exported symbols.
3841
context.enableSubstitution(SyntaxKind.PostfixUnaryExpression); // Substitutes updates to exported symbols.
3942
context.enableSubstitution(SyntaxKind.ShorthandPropertyAssignment); // Substitutes shorthand property assignments for imported/exported symbols.
@@ -46,6 +49,7 @@ namespace ts {
4649
let currentModuleInfo: ExternalModuleInfo; // The ExternalModuleInfo for the current file.
4750
let noSubstitution: boolean[]; // Set of nodes for which substitution rules should be ignored.
4851
let needUMDDynamicImportHelper: boolean;
52+
let bindingReferenceCache: ESMap<Node, Identifier | SourceFile | ImportClause | ImportSpecifier | undefined> | undefined;
4953

5054
return chainBundle(context, transformSourceFile);
5155

@@ -1742,6 +1746,10 @@ namespace ts {
17421746
return substituteExpressionIdentifier(<Identifier>node);
17431747
case SyntaxKind.BinaryExpression:
17441748
return substituteBinaryExpression(<BinaryExpression>node);
1749+
case SyntaxKind.CallExpression:
1750+
return substituteCallExpression(<CallExpression>node);
1751+
case SyntaxKind.TaggedTemplateExpression:
1752+
return substituteTaggedTemplateExpression(<TaggedTemplateExpression>node);
17451753
case SyntaxKind.PostfixUnaryExpression:
17461754
case SyntaxKind.PrefixUnaryExpression:
17471755
return substituteUnaryExpression(<PrefixUnaryExpression | PostfixUnaryExpression>node);
@@ -1751,57 +1759,124 @@ namespace ts {
17511759
}
17521760

17531761
/**
1754-
* Substitution for an Identifier expression that may contain an imported or exported
1755-
* symbol.
1756-
*
1757-
* @param node The node to substitute.
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`.
17581768
*/
1759-
function substituteExpressionIdentifier(node: Identifier): Expression {
1769+
function getImportOrExportBindingReferenceWorker(node: Identifier): Identifier | SourceFile | ImportClause | ImportSpecifier | undefined {
17601770
if (getEmitFlags(node) & EmitFlags.HelperName) {
17611771
const externalHelpersModuleName = getExternalHelpersModuleName(currentSourceFile);
17621772
if (externalHelpersModuleName) {
1763-
return factory.createPropertyAccessExpression(externalHelpersModuleName, node);
1773+
return externalHelpersModuleName;
1774+
}
1775+
}
1776+
else if (!(isGeneratedIdentifier(node) && !(node.autoGenerateFlags & GeneratedIdentifierFlags.AllowNameSubstitution)) && !isLocalName(node)) {
1777+
const exportContainer = resolver.getReferencedExportContainer(node, isExportName(node));
1778+
if (exportContainer?.kind === SyntaxKind.SourceFile) {
1779+
return exportContainer;
1780+
}
1781+
const importDeclaration = resolver.getReferencedImportDeclaration(node);
1782+
if (importDeclaration && (isImportClause(importDeclaration) || isImportSpecifier(importDeclaration))) {
1783+
return importDeclaration;
17641784
}
1785+
}
1786+
return undefined;
1787+
}
17651788

1766-
return node;
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)) {
1802+
result = getImportOrExportBindingReferenceWorker(node);
1803+
if (!removeEntry) {
1804+
bindingReferenceCache ||= new Map();
1805+
bindingReferenceCache.set(node, result);
1806+
}
1807+
}
1808+
else if (removeEntry) {
1809+
bindingReferenceCache?.delete(node);
17671810
}
1811+
return result;
1812+
}
17681813

1769-
if (!(isGeneratedIdentifier(node) && !(node.autoGenerateFlags & GeneratedIdentifierFlags.AllowNameSubstitution)) && !isLocalName(node)) {
1770-
const exportContainer = resolver.getReferencedExportContainer(node, isExportName(node));
1771-
if (exportContainer && exportContainer.kind === SyntaxKind.SourceFile) {
1814+
function substituteCallExpression(node: CallExpression) {
1815+
if (isIdentifier(node.expression) && getImportOrExportBindingReference(node.expression, /*removeEntry*/ false)) {
1816+
return isCallChain(node) ?
1817+
factory.updateCallChain(node,
1818+
setTextRange(factory.createComma(factory.createNumericLiteral(0), node.expression), node.expression),
1819+
node.questionDotToken,
1820+
/*typeArguments*/ undefined,
1821+
node.arguments) :
1822+
factory.updateCallExpression(node,
1823+
setTextRange(factory.createComma(factory.createNumericLiteral(0), node.expression), node.expression),
1824+
/*typeArguments*/ undefined,
1825+
node.arguments);
1826+
}
1827+
return node;
1828+
}
1829+
1830+
function substituteTaggedTemplateExpression(node: TaggedTemplateExpression) {
1831+
if (isIdentifier(node.tag) && getImportOrExportBindingReference(node.tag, /*removeEntry*/ false)) {
1832+
return factory.updateTaggedTemplateExpression(
1833+
node,
1834+
setTextRange(factory.createComma(factory.createNumericLiteral(0), node.tag), node.tag),
1835+
/*typeArguments*/ undefined,
1836+
node.template);
1837+
}
1838+
return node;
1839+
}
1840+
1841+
/**
1842+
* Substitution for an Identifier expression that may contain an imported or exported
1843+
* symbol.
1844+
*
1845+
* @param node The node to substitute.
1846+
*/
1847+
function substituteExpressionIdentifier(node: Identifier): Expression {
1848+
const result = getImportOrExportBindingReference(node, /*removeEntry*/ true);
1849+
switch (result?.kind) {
1850+
case SyntaxKind.Identifier: // tslib import
1851+
return factory.createPropertyAccessExpression(result, node);
1852+
case SyntaxKind.SourceFile: // top-level export
17721853
return setTextRange(
17731854
factory.createPropertyAccessExpression(
17741855
factory.createIdentifier("exports"),
17751856
factory.cloneNode(node)
17761857
),
17771858
/*location*/ node
17781859
);
1779-
}
1780-
1781-
const importDeclaration = resolver.getReferencedImportDeclaration(node);
1782-
if (importDeclaration) {
1783-
if (isImportClause(importDeclaration)) {
1784-
return setTextRange(
1785-
factory.createPropertyAccessExpression(
1786-
factory.getGeneratedNameForNode(importDeclaration.parent),
1787-
factory.createIdentifier("default")
1788-
),
1789-
/*location*/ node
1790-
);
1791-
}
1792-
else if (isImportSpecifier(importDeclaration)) {
1793-
const name = importDeclaration.propertyName || importDeclaration.name;
1794-
return setTextRange(
1795-
factory.createPropertyAccessExpression(
1796-
factory.getGeneratedNameForNode(importDeclaration.parent?.parent?.parent || importDeclaration),
1797-
factory.cloneNode(name)
1798-
),
1799-
/*location*/ node
1800-
);
1801-
}
1802-
}
1860+
case SyntaxKind.ImportClause:
1861+
return setTextRange(
1862+
factory.createPropertyAccessExpression(
1863+
factory.getGeneratedNameForNode(result.parent),
1864+
factory.createIdentifier("default")
1865+
),
1866+
/*location*/ node
1867+
);
1868+
case SyntaxKind.ImportSpecifier:
1869+
const name = result.propertyName || result.name;
1870+
return setTextRange(
1871+
factory.createPropertyAccessExpression(
1872+
factory.getGeneratedNameForNode(result.parent?.parent?.parent || result),
1873+
factory.cloneNode(name)
1874+
),
1875+
/*location*/ node
1876+
);
1877+
default:
1878+
return node;
18031879
}
1804-
return node;
18051880
}
18061881

18071882
/**

src/harness/evaluatorImpl.ts

Lines changed: 98 additions & 35 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"unittests/evaluation/asyncGenerator.ts",
9393
"unittests/evaluation/awaiter.ts",
9494
"unittests/evaluation/destructuring.ts",
95+
"unittests/evaluation/externalModules.ts",
9596
"unittests/evaluation/forAwaitOf.ts",
9697
"unittests/evaluation/forOf.ts",
9798
"unittests/evaluation/optionalCall.ts",

0 commit comments

Comments
 (0)