Skip to content

Commit 48ca7dc

Browse files
crisbetothePunderWoman
authored andcommitted
fix(compiler-cli): interpret string concat calls (#44167)
These changes add support for interpreting `String.prototype.concat` calls. We need to support it, because in TypeScript 4.5 string template expressions are transpiled to `concat` calls, rather than string concatenations. See microsoft/TypeScript#45304. PR Close #44167
1 parent 2e6b07a commit 48ca7dc

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import ts from 'typescript';
1010

1111
import {DynamicValue} from './dynamic';
12-
import {KnownFn, ResolvedValue, ResolvedValueArray} from './result';
12+
import {EnumValue, KnownFn, ResolvedValue, ResolvedValueArray} from './result';
1313

1414
export class ArraySliceBuiltinFn extends KnownFn {
1515
constructor(private lhs: ResolvedValueArray) {
@@ -45,6 +45,29 @@ export class ArrayConcatBuiltinFn extends KnownFn {
4545
}
4646
}
4747

48+
export class StringConcatBuiltinFn extends KnownFn {
49+
constructor(private lhs: string) {
50+
super();
51+
}
52+
53+
override evaluate(node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue {
54+
let result = this.lhs;
55+
for (const arg of args) {
56+
const resolved = arg instanceof EnumValue ? arg.resolved : arg;
57+
58+
if (typeof resolved === 'string' || typeof resolved === 'number' ||
59+
typeof resolved === 'boolean' || resolved == null) {
60+
// Cast to `any`, because `concat` will convert
61+
// anything to a string, but TS only allows strings.
62+
result = result.concat(resolved as any);
63+
} else {
64+
return DynamicValue.fromUnknown(node);
65+
}
66+
}
67+
return result;
68+
}
69+
}
70+
4871
export class ObjectAssignBuiltinFn extends KnownFn {
4972
override evaluate(node: ts.CallExpression, args: ResolvedValueArray): ResolvedValue {
5073
if (args.length === 0) {

packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {DependencyTracker} from '../../incremental/api';
1414
import {Declaration, DeclarationKind, DeclarationNode, EnumMember, FunctionDefinition, isConcreteDeclaration, ReflectionHost, SpecialDeclarationKind} from '../../reflection';
1515
import {isDeclaration} from '../../util/src/typescript';
1616

17-
import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn} from './builtin';
17+
import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn, StringConcatBuiltinFn} from './builtin';
1818
import {DynamicValue} from './dynamic';
1919
import {ForeignFunctionResolver} from './interface';
2020
import {resolveKnownDeclaration} from './known_declaration';
@@ -399,6 +399,8 @@ export class StaticInterpreter {
399399
return DynamicValue.fromInvalidExpressionType(node, rhs);
400400
}
401401
return lhs[rhs];
402+
} else if (typeof lhs === 'string' && rhs === 'concat') {
403+
return new StringConcatBuiltinFn(lhs);
402404
} else if (lhs instanceof Reference) {
403405
const ref = lhs.node;
404406
if (this.host.isClass(ref)) {

packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,17 @@ runInEachFileSystem(() => {
533533
.toBe('a.test.b');
534534
});
535535

536+
it('string `concat` function works', () => {
537+
expect(evaluate(`const a = '12', b = '34';`, 'a[\'concat\'](b)')).toBe('1234');
538+
expect(evaluate(`const a = '12', b = '3';`, 'a[\'concat\'](b)')).toBe('123');
539+
expect(evaluate(`const a = '12', b = '3', c = '45';`, 'a[\'concat\'](b,c)')).toBe('12345');
540+
expect(
541+
evaluate(`const a = '1', b = 2, c = '3', d = true, e = null;`, 'a[\'concat\'](b,c,d,e)'))
542+
.toBe('123truenull');
543+
expect(evaluate('enum Test { VALUE = "test" };', '"a."[\'concat\'](Test.VALUE, ".b")'))
544+
.toBe('a.test.b');
545+
});
546+
536547
it('should resolve non-literals as dynamic string', () => {
537548
const value = evaluate(`const a: any = [];`, '`a.${a}.b`');
538549

0 commit comments

Comments
 (0)