Skip to content

Commit 711b4e7

Browse files
authored
Indirect calls for imported functions (#44624)
* Indirect calls for imported functions * Fix unit tests
1 parent fafe3ff commit 711b4e7

File tree

177 files changed

+460
-311
lines changed

Some content is hidden

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

177 files changed

+460
-311
lines changed

src/compiler/emitter.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,7 +2473,17 @@ namespace ts {
24732473
}
24742474

24752475
function emitCallExpression(node: CallExpression) {
2476+
const indirectCall = getEmitFlags(node) & EmitFlags.IndirectCall;
2477+
if (indirectCall) {
2478+
writePunctuation("(");
2479+
writeLiteral("0");
2480+
writePunctuation(",");
2481+
writeSpace();
2482+
}
24762483
emitExpression(node.expression, parenthesizer.parenthesizeLeftSideOfAccess);
2484+
if (indirectCall) {
2485+
writePunctuation(")");
2486+
}
24772487
emit(node.questionDotToken);
24782488
emitTypeArguments(node, node.typeArguments);
24792489
emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments, parenthesizer.parenthesizeExpressionForDisallowedComma);
@@ -2488,7 +2498,17 @@ namespace ts {
24882498
}
24892499

24902500
function emitTaggedTemplateExpression(node: TaggedTemplateExpression) {
2501+
const indirectCall = getEmitFlags(node) & EmitFlags.IndirectCall;
2502+
if (indirectCall) {
2503+
writePunctuation("(");
2504+
writeLiteral("0");
2505+
writePunctuation(",");
2506+
writeSpace();
2507+
}
24912508
emitExpression(node.tag, parenthesizer.parenthesizeLeftSideOfAccess);
2509+
if (indirectCall) {
2510+
writePunctuation(")");
2511+
}
24922512
emitTypeArguments(node, node.typeArguments);
24932513
writeSpace();
24942514
emitExpression(node.template);

src/compiler/transformers/module/module.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ namespace ts {
3333
const previousOnEmitNode = context.onEmitNode;
3434
context.onSubstituteNode = onSubstituteNode;
3535
context.onEmitNode = onEmitNode;
36+
context.enableSubstitution(SyntaxKind.CallExpression); // Substitute calls to imported/exported symbols to avoid incorrect `this`.
37+
context.enableSubstitution(SyntaxKind.TaggedTemplateExpression); // Substitute calls to imported/exported symbols to avoid incorrect `this`.
3638
context.enableSubstitution(SyntaxKind.Identifier); // Substitutes expression identifiers with imported/exported symbols.
3739
context.enableSubstitution(SyntaxKind.BinaryExpression); // Substitutes assignments to exported symbols.
3840
context.enableSubstitution(SyntaxKind.PrefixUnaryExpression); // Substitutes updates to exported symbols.
@@ -1741,6 +1743,10 @@ namespace ts {
17411743
switch (node.kind) {
17421744
case SyntaxKind.Identifier:
17431745
return substituteExpressionIdentifier(node as Identifier);
1746+
case SyntaxKind.CallExpression:
1747+
return substituteCallExpression(node as CallExpression);
1748+
case SyntaxKind.TaggedTemplateExpression:
1749+
return substituteTaggedTemplateExpression(node as TaggedTemplateExpression);
17441750
case SyntaxKind.BinaryExpression:
17451751
return substituteBinaryExpression(node as BinaryExpression);
17461752
case SyntaxKind.PostfixUnaryExpression:
@@ -1751,6 +1757,43 @@ namespace ts {
17511757
return node;
17521758
}
17531759

1760+
function substituteCallExpression(node: CallExpression) {
1761+
if (isIdentifier(node.expression)) {
1762+
const expression = substituteExpressionIdentifier(node.expression);
1763+
noSubstitution[getNodeId(expression)] = true;
1764+
if (!isIdentifier(expression)) {
1765+
return addEmitFlags(
1766+
factory.updateCallExpression(node,
1767+
expression,
1768+
/*typeArguments*/ undefined,
1769+
node.arguments
1770+
),
1771+
EmitFlags.IndirectCall
1772+
);
1773+
1774+
}
1775+
}
1776+
return node;
1777+
}
1778+
1779+
function substituteTaggedTemplateExpression(node: TaggedTemplateExpression) {
1780+
if (isIdentifier(node.tag)) {
1781+
const tag = substituteExpressionIdentifier(node.tag);
1782+
noSubstitution[getNodeId(tag)] = true;
1783+
if (!isIdentifier(tag)) {
1784+
return addEmitFlags(
1785+
factory.updateTaggedTemplateExpression(node,
1786+
tag,
1787+
/*typeArguments*/ undefined,
1788+
node.template
1789+
),
1790+
EmitFlags.IndirectCall
1791+
);
1792+
}
1793+
}
1794+
return node;
1795+
}
1796+
17541797
/**
17551798
* Substitution for an Identifier expression that may contain an imported or exported
17561799
* symbol.

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6732,6 +6732,7 @@ namespace ts {
67326732
/*@internal*/ NeverApplyImportHelper = 1 << 26, // Indicates the node should never be wrapped with an import star helper (because, for example, it imports tslib itself)
67336733
/*@internal*/ IgnoreSourceNewlines = 1 << 27, // Overrides `printerOptions.preserveSourceNewlines` to print this node (and all descendants) with default whitespace.
67346734
/*@internal*/ Immutable = 1 << 28, // Indicates a node is a singleton intended to be reused in multiple locations. Any attempt to make further changes to the node will result in an error.
6735+
/*@internal*/ IndirectCall = 1 << 29, // Emit CallExpression as an indirect call: `(0, f)()`
67356736
}
67366737

67376738
export interface EmitHelperBase {

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"unittests/evaluation/asyncGenerator.ts",
9494
"unittests/evaluation/awaiter.ts",
9595
"unittests/evaluation/destructuring.ts",
96+
"unittests/evaluation/externalModules.ts",
9697
"unittests/evaluation/forAwaitOf.ts",
9798
"unittests/evaluation/forOf.ts",
9899
"unittests/evaluation/optionalCall.ts",
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
describe("unittests:: evaluation:: externalModules", () => {
2+
// https://github.com/microsoft/TypeScript/issues/35420
3+
it("Correct 'this' in function exported from external module", async () => {
4+
const result = evaluator.evaluateTypeScript({
5+
files: {
6+
"/.src/output.ts": `
7+
export const output: any[] = [];
8+
`,
9+
"/.src/other.ts": `
10+
import { output } from "./output";
11+
export function f(this: any, expected) {
12+
output.push(this === expected);
13+
}
14+
15+
// 0
16+
f(undefined);
17+
`,
18+
"/.src/main.ts": `
19+
export { output } from "./output";
20+
import { output } from "./output";
21+
import { f } from "./other";
22+
import * as other from "./other";
23+
24+
// 1
25+
f(undefined);
26+
27+
// 2
28+
const obj = {};
29+
f.call(obj, obj);
30+
31+
// 3
32+
other.f(other);
33+
`
34+
},
35+
rootFiles: ["/.src/main.ts"],
36+
main: "/.src/main.ts"
37+
});
38+
assert.equal(result.output[0], true); // `f(undefined)` inside module. Existing behavior is correct.
39+
assert.equal(result.output[1], true); // `f(undefined)` from import. New behavior to match first case.
40+
assert.equal(result.output[2], true); // `f.call(obj, obj)`. Behavior of `.call` (or `.apply`, etc.) should not be affected.
41+
assert.equal(result.output[3], true); // `other.f(other)`. `this` is still namespace because it is left of `.`.
42+
});
43+
44+
it("Correct 'this' in function expression exported from external module", async () => {
45+
const result = evaluator.evaluateTypeScript({
46+
files: {
47+
"/.src/output.ts": `
48+
export const output: any[] = [];
49+
`,
50+
"/.src/other.ts": `
51+
import { output } from "./output";
52+
export const f = function(this: any, expected) {
53+
output.push(this === expected);
54+
}
55+
56+
// 0
57+
f(undefined);
58+
`,
59+
"/.src/main.ts": `
60+
export { output } from "./output";
61+
import { output } from "./output";
62+
import { f } from "./other";
63+
import * as other from "./other";
64+
65+
// 1
66+
f(undefined);
67+
68+
// 2
69+
const obj = {};
70+
f.call(obj, obj);
71+
72+
// 3
73+
other.f(other);
74+
`
75+
},
76+
rootFiles: ["/.src/main.ts"],
77+
main: "/.src/main.ts"
78+
});
79+
assert.equal(result.output[0], true); // `f(undefined)` inside module. Existing behavior is incorrect.
80+
assert.equal(result.output[1], true); // `f(undefined)` from import. New behavior to match first case.
81+
assert.equal(result.output[2], true); // `f.call(obj, obj)`. Behavior of `.call` (or `.apply`, etc.) should not be affected.
82+
assert.equal(result.output[3], true); // `other.f(other)`. `this` is still namespace because it is left of `.`.
83+
});
84+
});

src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ exports.fn3 = fn3;`;
5555
content: `"use strict";
5656
exports.__esModule = true;${appendJsText === changeJs ? "\nexports.fn3 = void 0;" : ""}
5757
var fns_1 = require("../decls/fns");
58-
fns_1.fn1();
59-
fns_1.fn2();
58+
(0, fns_1.fn1)();
59+
(0, fns_1.fn2)();
6060
${appendJs}`
6161
}];
6262
}

tests/baselines/reference/allowJscheckJsTypeParameterNoCrash.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ exports.__esModule = true;
3030
exports.a = void 0;
3131
var func_1 = require("./func");
3232
// hover on vextend
33-
exports.a = func_1.vextend({
33+
exports.a = (0, func_1.vextend)({
3434
watch: {
3535
data1: function (val) {
3636
this.data2 = 1;

tests/baselines/reference/allowSyntheticDefaultImportsCanPaintCrossModuleDeclaration.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ exports.__esModule = true;
2323
exports.__esModule = true;
2424
exports.A = void 0;
2525
var file1_1 = require("./file1");
26-
exports.A = file1_1.styled();
26+
exports.A = (0, file1_1.styled)();
2727

2828

2929
//// [color.d.ts]

tests/baselines/reference/ambientDeclarationsPatterns.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ foo(fileText);
3737
exports.__esModule = true;
3838
///<reference path="declarations.d.ts" />
3939
var foobarbaz_1 = require("foobarbaz");
40-
foobarbaz_1.foo(foobarbaz_1.baz);
40+
(0, foobarbaz_1.foo)(foobarbaz_1.baz);
4141
var foosball_1 = require("foosball");
42-
foobarbaz_1.foo(foosball_1.foos);
42+
(0, foobarbaz_1.foo)(foosball_1.foos);
4343
// Works with relative file name
4444
var file_text_1 = require("./file!text");
45-
foobarbaz_1.foo(file_text_1["default"]);
45+
(0, foobarbaz_1.foo)(file_text_1["default"]);

tests/baselines/reference/ambientShorthand.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ exports.__esModule = true;
2020
var jquery_1 = require("jquery");
2121
var baz = require("fs");
2222
var boom = require("jquery");
23-
jquery_1["default"](jquery_1.bar, baz, boom);
23+
(0, jquery_1["default"])(jquery_1.bar, baz, boom);

0 commit comments

Comments
 (0)