Skip to content

Commit

Permalink
fix(compiler-cli): evaluate const tuple types statically (#48091)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
JoostK authored and AndrewKushnir committed Dec 7, 2022
1 parent f273666 commit b55d2da
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 0 deletions.
Expand Up @@ -704,6 +704,10 @@ export class StaticInterpreter {
return this.visitTupleType(node, context);
} else if (ts.isNamedTupleMember(node)) {
return this.visitType(node.type, context);
} else if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
return this.visitType(node.type, context);
} else if (ts.isTypeQueryNode(node)) {
return this.visitTypeQuery(node, context);
}

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

return res;
}

private visitTypeQuery(node: ts.TypeQueryNode, context: Context): ResolvedValue {
if (!ts.isIdentifier(node.exprName)) {
return DynamicValue.fromUnknown(node);
}

const decl = this.host.getDeclarationOfIdentifier(node.exprName);
if (decl === null) {
return DynamicValue.fromUnknownIdentifier(node.exprName);
}

const declContext: Context = {...context, ...joinModuleContext(context, node, decl)};
return this.visitAmbiguousDeclaration(decl, declContext);
}
}

function isFunctionOrMethodReference(ref: Reference<ts.Node>):
Expand Down
Expand Up @@ -371,6 +371,38 @@ runInEachFileSystem(() => {
expect(evaluate(`declare const x: ['bar'];`, `[...x]`)).toEqual(['bar']);
});

// https://github.com/angular/angular/issues/48089
it('supports declarations of readonly tuples with class references', () => {
const tuple = evaluate(
`
import {External} from 'external';
declare class Local {}
declare const x: readonly [typeof External, typeof Local];`,
`x`, [
{
name: _('/node_modules/external/index.d.ts'),
contents: 'export declare class External {}'
},
]);
if (!Array.isArray(tuple)) {
return fail('Should have evaluated tuple as an array');
}
const [external, local] = tuple;
if (!(external instanceof Reference)) {
return fail('Should have evaluated `typeof A` to a Reference');
}
expect(ts.isClassDeclaration(external.node)).toBe(true);
expect(external.debugName).toBe('External');
expect(external.ownedByModuleGuess).toBe('external');

if (!(local instanceof Reference)) {
return fail('Should have evaluated `typeof B` to a Reference');
}
expect(ts.isClassDeclaration(local.node)).toBe(true);
expect(local.debugName).toBe('Local');
expect(local.ownedByModuleGuess).toBeNull();
});

it('evaluates tuple elements it cannot understand to DynamicValue', () => {
const value = evaluate(`declare const x: ['foo', string];`, `x`) as [string, DynamicValue];

Expand Down
29 changes: 29 additions & 0 deletions packages/compiler-cli/test/ngtsc/standalone_spec.ts
Expand Up @@ -870,6 +870,35 @@ runInEachFileSystem(() => {
const jsCode = env.getContents('test.js');
expect(jsCode).toContain('dependencies: [StandalonePipe]');
});

it('should compile imports using a const tuple in external library', () => {
env.write('node_modules/external/index.d.ts', `
import {ɵɵDirectiveDeclaration} from '@angular/core';
export declare class StandaloneDir {
static ɵdir: ɵɵDirectiveDeclaration<StandaloneDir, "[dir]", never, {}, {}, never, never, true>;
}
export declare const DECLARATIONS: readonly [typeof StandaloneDir];
`);
env.write('test.ts', `
import {Component, Directive} from '@angular/core';
import {DECLARATIONS} from 'external';
@Component({
standalone: true,
selector: 'test-cmp',
template: '<div dir></div>',
imports: [DECLARATIONS],
})
export class TestCmp {}
`);
env.driveMain();

const jsCode = env.getContents('test.js');
expect(jsCode).toContain('import * as i1 from "external";');
expect(jsCode).toContain('dependencies: [i1.StandaloneDir]');
});
});

describe('optimizations', () => {
Expand Down

0 comments on commit b55d2da

Please sign in to comment.