Skip to content

Commit f8c02b6

Browse files
devversionthePunderWoman
authored andcommitted
refactor(compiler-cli): restrict read option values for signal-based queries (angular#54257)
The `read` option for queries can rely on lexical variables inside the class. These constructs are fine from a technical perspective in TypeScript, but in practice, when the component/directive definition is being created, the read value is extracted into the definition, **outside** of the class. This breaks `this` references. To fix this, we are restricting the `read` option to literal values. Similar to `descendants`. Literal references are in practice constructs like: - `read: bla.X` - `read: X` where `bla` or `X` is never a `ThisKeywoord`- hence fixing the issue and also simplifying the patterns for easier single file compilation. PR Close angular#54257
1 parent 902481b commit f8c02b6

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,26 @@ function parseLocator(expression: ts.Expression, reflector: ReflectionHost): str
9494
unwrappedExpression !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None);
9595
}
9696

97-
/** Parses the `read` option of a query. */
97+
/**
98+
* Parses the `read` option of a query.
99+
*
100+
* We only support the following patterns for the `read` option:
101+
* - `read: someImport.BLA`,
102+
* - `read: BLA`
103+
*
104+
* That is because we cannot trivially support complex expressions,
105+
* especially those referencing `this`. The read provider token will
106+
* live outside of the class in the static class definition.
107+
*/
98108
function parseReadOption(value: ts.Expression): o.Expression {
99-
return new o.WrappedNodeExpr(value);
109+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) ||
110+
ts.isIdentifier(value)) {
111+
return new o.WrappedNodeExpr(value);
112+
}
113+
114+
throw new FatalDiagnosticError(
115+
ErrorCode.VALUE_NOT_LITERAL, value,
116+
`Query "read" option expected a literal class reference.`);
100117
}
101118

102119
/** Parses the `descendants` option of a query. */

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ runInEachFileSystem(() => {
3838
expect(js).toContain(`i0.ɵɵqueryAdvance();`);
3939
});
4040

41+
it('should support viewChild with `read` options', () => {
42+
env.write('other-file.ts', `export class X {}`);
43+
env.write('test.ts', `
44+
import {Component, viewChild} from '@angular/core';
45+
import * as fromOtherFile from './other-file';
46+
47+
class X {}
48+
49+
@Component({selector: 'test', template: ''})
50+
export class TestDir {
51+
el = viewChild('myLocator', {read: X});
52+
el2 = viewChild('myLocator', {read: fromOtherFile.X});
53+
}
54+
`);
55+
env.driveMain();
56+
57+
const js = env.getContents('test.js');
58+
expect(js).toContain(`i0.ɵɵviewQuerySignal(ctx.el, _c0, 5, X);`);
59+
expect(js).toContain(`i0.ɵɵviewQuerySignal(ctx.el2, _c0, 5, fromOtherFile.X);`);
60+
expect(js).toContain(`i0.ɵɵqueryAdvance(2);`);
61+
});
62+
4163
it('should handle a basic viewChildren', () => {
4264
env.write('test.ts', `
4365
import {Component, viewChildren} from '@angular/core';
@@ -184,6 +206,25 @@ runInEachFileSystem(() => {
184206
messageText: `Decorator query metadata must be an instance of a query type`,
185207
})]);
186208
});
209+
210+
it('should report an error when `read` option is complex', () => {
211+
env.write('test.ts', `
212+
import {Directive, ViewChild, viewChild} from '@angular/core';
213+
214+
@Directive({
215+
selector: 'test',
216+
})
217+
export class TestDir {
218+
something = null!;
219+
el = viewChild('myLocator', {read: this.something});
220+
}
221+
`);
222+
const diagnostics = env.driveDiagnostics();
223+
expect(diagnostics.length).toBe(1);
224+
expect(diagnostics).toEqual([jasmine.objectContaining({
225+
messageText: `Query "read" option expected a literal class reference.`,
226+
})]);
227+
});
187228
});
188229
});
189230
});

0 commit comments

Comments
 (0)