Skip to content

Commit 78188e8

Browse files
crisbetodylhunn
authored andcommitted
fix(compiler-cli): add diagnostic if initializer API is used outside of an initializer (#54993)
Adds a rule that will produce a diagnostic when an initializer-based API is used outside of an initializer. Fixes #54381. PR Close #54993
1 parent 8226be6 commit 78188e8

10 files changed

Lines changed: 363 additions & 52 deletions

File tree

goldens/public-api/compiler-cli/error_code.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export enum ErrorCode {
100100
TEXT_ATTRIBUTE_NOT_BINDING = 8104,
101101
UNDECORATED_CLASS_USING_ANGULAR_FEATURES = 2007,
102102
UNDECORATED_PROVIDER = 2005,
103+
UNSUPPORTED_INITIALIZER_API_USAGE = 8110,
103104
// (undocumented)
104105
VALUE_HAS_WRONG_TYPE = 1010,
105106
// (undocumented)

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

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,25 @@ import {InputMapping} from '../../../metadata';
1313
import {ClassMember, ClassMemberAccessLevel, ReflectionHost} from '../../../reflection';
1414

1515
import {validateAccessOfInitializerApiMember} from './initializer_function_access';
16-
import {tryParseInitializerApi} from './initializer_functions';
16+
import {InitializerApiFunction, tryParseInitializerApi} from './initializer_functions';
1717
import {parseAndValidateInputAndOutputOptions} from './input_output_parse_options';
1818

19+
/** Represents a function that can declare an input. */
20+
export const INPUT_INITIALIZER_FN: InitializerApiFunction = {
21+
functionName: 'input',
22+
owningModule: '@angular/core',
23+
// Inputs are accessed from parents, via the `property` instruction.
24+
// Conceptually, the fields need to be publicly readable, but in practice,
25+
// accessing `protected` or `private` members works at runtime, so we can allow
26+
// cases where the input is intentionally not part of the public API, programmatically.
27+
// Note: `private` is omitted intentionally as this would be a conceptual confusion point.
28+
allowedAccessLevels: [
29+
ClassMemberAccessLevel.PublicWritable,
30+
ClassMemberAccessLevel.PublicReadonly,
31+
ClassMemberAccessLevel.Protected,
32+
],
33+
};
34+
1935
/**
2036
* Attempts to parse a signal input class member. Returns the parsed
2137
* input mapping if possible.
@@ -27,22 +43,8 @@ export function tryParseSignalInputMapping(
2743
return null;
2844
}
2945

30-
const signalInput = tryParseInitializerApi(
31-
[{
32-
functionName: 'input',
33-
owningModule: '@angular/core',
34-
// Inputs are accessed from parents, via the `property` instruction.
35-
// Conceptually, the fields need to be publicly readable, but in practice,
36-
// accessing `protected` or `private` members works at runtime, so we can allow
37-
// cases where the input is intentionally not part of the public API, programmatically.
38-
// Note: `private` is omitted intentionally as this would be a conceptual confusion point.
39-
allowedAccessLevels: [
40-
ClassMemberAccessLevel.PublicWritable,
41-
ClassMemberAccessLevel.PublicReadonly,
42-
ClassMemberAccessLevel.Protected,
43-
],
44-
}],
45-
member.value, reflector, importTracker);
46+
const signalInput =
47+
tryParseInitializerApi([INPUT_INITIALIZER_FN], member.value, reflector, importTracker);
4648
if (signalInput === null) {
4749
return null;
4850
}

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,24 @@ import {ModelMapping} from '../../../metadata';
1313
import {ClassMember, ClassMemberAccessLevel, ReflectionHost} from '../../../reflection';
1414

1515
import {validateAccessOfInitializerApiMember} from './initializer_function_access';
16-
import {tryParseInitializerApi} from './initializer_functions';
16+
import {InitializerApiFunction, tryParseInitializerApi} from './initializer_functions';
1717
import {parseAndValidateInputAndOutputOptions} from './input_output_parse_options';
1818

19+
/** Represents a function that can declare a model. */
20+
export const MODEL_INITIALIZER_FN: InitializerApiFunction = {
21+
functionName: 'model',
22+
owningModule: '@angular/core',
23+
// Inputs are accessed from parents, via the `property` instruction.
24+
// Conceptually, the fields need to be publicly readable, but in practice,
25+
// accessing `protected` or `private` members works at runtime, so we can allow
26+
// cases where the input is intentionally not part of the public API, programmatically.
27+
allowedAccessLevels: [
28+
ClassMemberAccessLevel.PublicWritable,
29+
ClassMemberAccessLevel.PublicReadonly,
30+
ClassMemberAccessLevel.Protected,
31+
],
32+
};
33+
1934
/**
2035
* Attempts to parse a model class member. Returns the parsed model mapping if possible.
2136
*/
@@ -26,21 +41,8 @@ export function tryParseSignalModelMapping(
2641
return null;
2742
}
2843

29-
const model = tryParseInitializerApi(
30-
[{
31-
functionName: 'model',
32-
owningModule: '@angular/core',
33-
// Inputs are accessed from parents, via the `property` instruction.
34-
// Conceptually, the fields need to be publicly readable, but in practice,
35-
// accessing `protected` or `private` members works at runtime, so we can allow
36-
// cases where the input is intentionally not part of the public API, programmatically.
37-
allowedAccessLevels: [
38-
ClassMemberAccessLevel.PublicWritable,
39-
ClassMemberAccessLevel.PublicReadonly,
40-
ClassMemberAccessLevel.Protected,
41-
],
42-
}],
43-
member.value, reflector, importTracker);
44+
const model =
45+
tryParseInitializerApi([MODEL_INITIALIZER_FN], member.value, reflector, importTracker);
4446
if (model === null) {
4547
return null;
4648
}

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {InputOrOutput} from '../../../metadata';
1414
import {ClassMember, ClassMemberAccessLevel, ReflectionHost} from '../../../reflection';
1515

1616
import {validateAccessOfInitializerApiMember} from './initializer_function_access';
17-
import {tryParseInitializerApi} from './initializer_functions';
17+
import {InitializerApiFunction, tryParseInitializerApi} from './initializer_functions';
1818
import {parseAndValidateInputAndOutputOptions} from './input_output_parse_options';
1919

2020
// Outputs are accessed from parents, via the `listener` instruction.
@@ -29,6 +29,20 @@ const allowedAccessLevels = [
2929
ClassMemberAccessLevel.Protected,
3030
];
3131

32+
/** Possible functions that can declare an output. */
33+
export const OUTPUT_INITIALIZER_FNS: InitializerApiFunction[] = [
34+
{
35+
functionName: 'output',
36+
owningModule: '@angular/core',
37+
allowedAccessLevels,
38+
},
39+
{
40+
functionName: 'outputFromObservable',
41+
owningModule: '@angular/core/rxjs-interop',
42+
allowedAccessLevels
43+
},
44+
];
45+
3246
/**
3347
* Attempts to parse a signal output class member. Returns the parsed
3448
* input mapping if possible.
@@ -41,20 +55,8 @@ export function tryParseInitializerBasedOutput(
4155
return null;
4256
}
4357

44-
const output = tryParseInitializerApi(
45-
[
46-
{
47-
functionName: 'output',
48-
owningModule: '@angular/core',
49-
allowedAccessLevels,
50-
},
51-
{
52-
functionName: 'outputFromObservable',
53-
owningModule: '@angular/core/rxjs-interop',
54-
allowedAccessLevels
55-
},
56-
],
57-
member.value, reflector, importTracker);
58+
const output =
59+
tryParseInitializerApi(OUTPUT_INITIALIZER_FNS, member.value, reflector, importTracker);
5860
if (output === null) {
5961
return null;
6062
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const queryFunctionNames: QueryFunctionName[] =
2626
['viewChild', 'viewChildren', 'contentChild', 'contentChildren'];
2727

2828
/** Possible query initializer API functions. */
29-
const initializerFns = queryFunctionNames.map(
29+
export const QUERY_INITIALIZER_FNS = queryFunctionNames.map(
3030
fnName => ({
3131
functionName: fnName,
3232
owningModule: '@angular/core' as const,
@@ -61,7 +61,8 @@ export function tryParseSignalQueryFromInitializer(
6161
return null;
6262
}
6363

64-
const query = tryParseInitializerApi(initializerFns, member.value, reflector, importTracker);
64+
const query =
65+
tryParseInitializerApi(QUERY_INITIALIZER_FNS, member.value, reflector, importTracker);
6566
if (query === null) {
6667
return null;
6768
}

packages/compiler-cli/src/ngtsc/annotations/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
export {forwardRefResolver, getAngularDecorators, isAngularDecorator, NoopReferencesRegistry, ReferencesRegistry, ResourceLoader, ResourceLoaderContext} from './common';
1212
export {ComponentDecoratorHandler} from './component';
13-
export {DirectiveDecoratorHandler, InitializerApiFunction, queryDecoratorNames, QueryFunctionName, tryParseInitializerApi, tryParseInitializerBasedOutput, tryParseSignalInputMapping, tryParseSignalModelMapping, tryParseSignalQueryFromInitializer} from './directive';
13+
export {DirectiveDecoratorHandler, InitializerApiFunction, INPUT_INITIALIZER_FN, MODEL_INITIALIZER_FN, OUTPUT_INITIALIZER_FNS, QUERY_INITIALIZER_FNS, queryDecoratorNames, QueryFunctionName, tryParseInitializerApi, tryParseInitializerBasedOutput, tryParseSignalInputMapping, tryParseSignalModelMapping, tryParseSignalQueryFromInitializer} from './directive';
1414
export {NgModuleDecoratorHandler} from './ng_module';
1515
export {InjectableDecoratorHandler} from './src/injectable';
1616
export {PipeDecoratorHandler} from './src/pipe';

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,21 @@ export enum ErrorCode {
459459
*/
460460
INTERPOLATED_SIGNAL_NOT_INVOKED = 8109,
461461

462+
/**
463+
* Initializer-based APIs can only be invoked from inside of an initializer.
464+
*
465+
* ```
466+
* // Allowed
467+
* myInput = input();
468+
*
469+
* // Not allowed
470+
* function myInput() {
471+
* return input();
472+
* }
473+
* ```
474+
*/
475+
UNSUPPORTED_INITIALIZER_API_USAGE = 8110,
476+
462477
/**
463478
* The template type-checking engine would need to generate an inline type check block for a
464479
* component, but the current type-checking environment doesn't support it.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*!
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
import {InitializerApiFunction, INPUT_INITIALIZER_FN, MODEL_INITIALIZER_FN, OUTPUT_INITIALIZER_FNS, QUERY_INITIALIZER_FNS, tryParseInitializerApi} from '../../../annotations';
12+
import {ErrorCode, makeDiagnostic} from '../../../diagnostics';
13+
import {ImportedSymbolsTracker} from '../../../imports';
14+
import {ReflectionHost} from '../../../reflection';
15+
16+
import {SourceFileValidatorRule} from './api';
17+
18+
/** APIs whose usages should be checked by the rule. */
19+
const APIS_TO_CHECK: InitializerApiFunction[] = [
20+
INPUT_INITIALIZER_FN,
21+
MODEL_INITIALIZER_FN,
22+
...OUTPUT_INITIALIZER_FNS,
23+
...QUERY_INITIALIZER_FNS,
24+
];
25+
26+
/**
27+
* Rule that flags any initializer APIs that are used outside of an initializer.
28+
*/
29+
export class InitializerApiUsageRule implements SourceFileValidatorRule {
30+
constructor(
31+
private reflector: ReflectionHost, private importedSymbolsTracker: ImportedSymbolsTracker) {}
32+
33+
shouldCheck(sourceFile: ts.SourceFile): boolean {
34+
// Skip the traversal if there are no imports of the initializer APIs.
35+
return APIS_TO_CHECK.some(({functionName, owningModule}) => {
36+
return this.importedSymbolsTracker.hasNamedImport(sourceFile, functionName, owningModule) ||
37+
this.importedSymbolsTracker.hasNamespaceImport(sourceFile, owningModule);
38+
});
39+
}
40+
41+
checkNode(node: ts.Node): ts.Diagnostic[]|null {
42+
// We only care about call expressions.
43+
if (!ts.isCallExpression(node)) {
44+
return null;
45+
}
46+
47+
// Unwrap any parenthesized and `as` expressions since they don't affect the runtime behavior.
48+
while (node.parent &&
49+
(ts.isParenthesizedExpression(node.parent) || ts.isAsExpression(node.parent))) {
50+
node = node.parent;
51+
}
52+
53+
// Initializer functions are allowed to be used in the initializer.
54+
if (!node.parent || !ts.isCallExpression(node) ||
55+
(ts.isPropertyDeclaration(node.parent) && node.parent.initializer === node)) {
56+
return null;
57+
}
58+
59+
const identifiedInitializer =
60+
tryParseInitializerApi(APIS_TO_CHECK, node, this.reflector, this.importedSymbolsTracker);
61+
if (identifiedInitializer === null) {
62+
return null;
63+
}
64+
65+
return [
66+
makeDiagnostic(
67+
ErrorCode.UNSUPPORTED_INITIALIZER_API_USAGE, node,
68+
`Unsupported call to the ${identifiedInitializer.api.functionName}${
69+
identifiedInitializer.isRequired ?
70+
'.required' :
71+
''} function. This function can only be called in the initializer of a class member.`)
72+
];
73+
}
74+
}

packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {ImportedSymbolsTracker} from '../../imports';
1212
import {ReflectionHost} from '../../reflection';
1313

1414
import {SourceFileValidatorRule} from './rules/api';
15+
import {InitializerApiUsageRule} from './rules/initializer_api_usage_rule';
1516

1617
/**
1718
* Validates that TypeScript files match a specific set of rules set by the Angular compiler.
@@ -20,7 +21,7 @@ export class SourceFileValidator {
2021
private rules: SourceFileValidatorRule[];
2122

2223
constructor(reflector: ReflectionHost, importedSymbolsTracker: ImportedSymbolsTracker) {
23-
this.rules = []; // TODO: implement the rules.
24+
this.rules = [new InitializerApiUsageRule(reflector, importedSymbolsTracker)];
2425
}
2526

2627
/**

0 commit comments

Comments
 (0)