Skip to content

Commit b55d2da

Browse files
JoostKAndrewKushnir
authored andcommitted
fix(compiler-cli): evaluate const tuple types statically (#48091)
For standalone components it may be beneficial to group multiple declarations into a single array, that can then be imported all at once in `Component.imports`. If this array is declared within a library, however, would the AOT compiler need to extract the contents of the array from the declaration file. This requires that the array is constructed using an `as const` cast, which results in a readonly tuple declaration in the generated .d.ts file of the library: ```ts export declare const DECLARATIONS: readonly [typeof StandaloneDir]; ``` The partial evaluator logic did not support this syntax, so this pattern was not functional when a library is involved. This commit adds the necessary logic in the static interpreter to evaluate this type at compile time. Closes #48089 PR Close #48091
1 parent f273666 commit b55d2da

File tree

3 files changed

+79
-0
lines changed

3 files changed

+79
-0
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,10 @@ export class StaticInterpreter {
704704
return this.visitTupleType(node, context);
705705
} else if (ts.isNamedTupleMember(node)) {
706706
return this.visitType(node.type, context);
707+
} else if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
708+
return this.visitType(node.type, context);
709+
} else if (ts.isTypeQueryNode(node)) {
710+
return this.visitTypeQuery(node, context);
707711
}
708712

709713
return DynamicValue.fromDynamicType(node);
@@ -718,6 +722,20 @@ export class StaticInterpreter {
718722

719723
return res;
720724
}
725+
726+
private visitTypeQuery(node: ts.TypeQueryNode, context: Context): ResolvedValue {
727+
if (!ts.isIdentifier(node.exprName)) {
728+
return DynamicValue.fromUnknown(node);
729+
}
730+
731+
const decl = this.host.getDeclarationOfIdentifier(node.exprName);
732+
if (decl === null) {
733+
return DynamicValue.fromUnknownIdentifier(node.exprName);
734+
}
735+
736+
const declContext: Context = {...context, ...joinModuleContext(context, node, decl)};
737+
return this.visitAmbiguousDeclaration(decl, declContext);
738+
}
721739
}
722740

723741
function isFunctionOrMethodReference(ref: Reference<ts.Node>):

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,38 @@ runInEachFileSystem(() => {
371371
expect(evaluate(`declare const x: ['bar'];`, `[...x]`)).toEqual(['bar']);
372372
});
373373

374+
// https://github.com/angular/angular/issues/48089
375+
it('supports declarations of readonly tuples with class references', () => {
376+
const tuple = evaluate(
377+
`
378+
import {External} from 'external';
379+
declare class Local {}
380+
declare const x: readonly [typeof External, typeof Local];`,
381+
`x`, [
382+
{
383+
name: _('/node_modules/external/index.d.ts'),
384+
contents: 'export declare class External {}'
385+
},
386+
]);
387+
if (!Array.isArray(tuple)) {
388+
return fail('Should have evaluated tuple as an array');
389+
}
390+
const [external, local] = tuple;
391+
if (!(external instanceof Reference)) {
392+
return fail('Should have evaluated `typeof A` to a Reference');
393+
}
394+
expect(ts.isClassDeclaration(external.node)).toBe(true);
395+
expect(external.debugName).toBe('External');
396+
expect(external.ownedByModuleGuess).toBe('external');
397+
398+
if (!(local instanceof Reference)) {
399+
return fail('Should have evaluated `typeof B` to a Reference');
400+
}
401+
expect(ts.isClassDeclaration(local.node)).toBe(true);
402+
expect(local.debugName).toBe('Local');
403+
expect(local.ownedByModuleGuess).toBeNull();
404+
});
405+
374406
it('evaluates tuple elements it cannot understand to DynamicValue', () => {
375407
const value = evaluate(`declare const x: ['foo', string];`, `x`) as [string, DynamicValue];
376408

packages/compiler-cli/test/ngtsc/standalone_spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,35 @@ runInEachFileSystem(() => {
870870
const jsCode = env.getContents('test.js');
871871
expect(jsCode).toContain('dependencies: [StandalonePipe]');
872872
});
873+
874+
it('should compile imports using a const tuple in external library', () => {
875+
env.write('node_modules/external/index.d.ts', `
876+
import {ɵɵDirectiveDeclaration} from '@angular/core';
877+
878+
export declare class StandaloneDir {
879+
static ɵdir: ɵɵDirectiveDeclaration<StandaloneDir, "[dir]", never, {}, {}, never, never, true>;
880+
}
881+
882+
export declare const DECLARATIONS: readonly [typeof StandaloneDir];
883+
`);
884+
env.write('test.ts', `
885+
import {Component, Directive} from '@angular/core';
886+
import {DECLARATIONS} from 'external';
887+
888+
@Component({
889+
standalone: true,
890+
selector: 'test-cmp',
891+
template: '<div dir></div>',
892+
imports: [DECLARATIONS],
893+
})
894+
export class TestCmp {}
895+
`);
896+
env.driveMain();
897+
898+
const jsCode = env.getContents('test.js');
899+
expect(jsCode).toContain('import * as i1 from "external";');
900+
expect(jsCode).toContain('dependencies: [i1.StandaloneDir]');
901+
});
873902
});
874903

875904
describe('optimizations', () => {

0 commit comments

Comments
 (0)