88
99import ts from 'typescript' ;
1010
11- import { ImportedSymbolsTracker } from '../../../imports' ;
1211import { ClassMember , ReflectionHost } from '../../../reflection' ;
1312import { CORE_MODULE } from '../../common' ;
1413
@@ -40,18 +39,6 @@ interface InitializerFunctionMetadata {
4039 isRequired : boolean ;
4140}
4241
43- /**
44- * Metadata that can be inferred from an initializer
45- * statically without going through the type checker.
46- */
47- interface StaticInitializerData {
48- /** Identifier in the initializer that refers to the Angular API. */
49- node : ts . Identifier ;
50-
51- /** Whether the call is required. */
52- isRequired : boolean ;
53- }
54-
5542/**
5643 * Attempts to identify an Angular class member that is declared via
5744 * its initializer referring to a given initializer API function.
@@ -61,110 +48,97 @@ interface StaticInitializerData {
6148 */
6249export function tryParseInitializerApiMember < FnNames extends InitializerApiFunction [ ] > (
6350 fnNames : FnNames , member : Pick < ClassMember , 'value' > , reflector : ReflectionHost ,
64- importTracker : ImportedSymbolsTracker ) : InitializerFunctionMetadata | null {
51+ isCore : boolean ) : InitializerFunctionMetadata & { apiName : FnNames [ number ] } | null {
6552 if ( member . value === null || ! ts . isCallExpression ( member . value ) ) {
6653 return null ;
6754 }
68-
6955 const call = member . value ;
70- const staticResult = parseTopLevelCall ( call , fnNames , importTracker ) ||
71- parseTopLevelRequiredCall ( call , fnNames , importTracker ) ||
72- parseTopLevelCallFromNamespace ( call , fnNames , importTracker ) ;
7356
74- if ( staticResult === null ) {
57+ // Extract target. Either:
58+ // - `[input]`
59+ // - `core.[input]`
60+ // - `input.[required]`
61+ // - `core.input.[required]`.
62+ let target = extractPropertyTarget ( call . expression ) ;
63+ if ( target === null ) {
7564 return null ;
7665 }
7766
78- // Once we've statically determined that the initializer is one of the APIs we're looking for, we
79- // need to verify it using the type checker which accounts for things like shadowed variables.
80- // This should be done as the absolute last step since using the type check can be expensive.
81- const resolvedImport = reflector . getImportOfIdentifier ( staticResult . node ) ;
82- if ( resolvedImport === null || ! ( fnNames as string [ ] ) . includes ( resolvedImport . name ) ) {
67+ // Find if the `target` matches one of the expected APIs we are looking for.
68+ // e.g. `input`, or `viewChild`.
69+ let apiName = fnNames . find ( n => n === target ! . text ) ;
70+
71+ // Case 1: API is directly called. e.g. `input`
72+ // If no API name was matched, continue looking for `input.required`.
73+ if ( apiName !== undefined ) {
74+ if ( ! isReferenceToInitializerApiFunction ( apiName , target , isCore , reflector ) ) {
75+ return null ;
76+ }
77+ return { apiName, call, isRequired : false } ;
78+ }
79+
80+ // Case 2: API is the `.required`
81+ // Ensure there is a property access to `[input].required` or `[core.input].required`.
82+ if ( target . text !== 'required' || ! ts . isPropertyAccessExpression ( call . expression ) ) {
8383 return null ;
8484 }
8585
86- return {
87- call,
88- isRequired : staticResult . isRequired ,
89- apiName : resolvedImport . name as InitializerApiFunction ,
90- } ;
91- }
86+ // e.g. `[input.required]` (the full property access is this)
87+ const apiPropertyAccess = call . expression ;
88+ // e.g. `[input].required` (we now extract the left side of the access).
89+ target = extractPropertyTarget ( apiPropertyAccess . expression ) ;
90+ if ( target === null ) {
91+ return null ;
92+ }
9293
93- /**
94- * Attempts to parse a top-level call to an initializer function,
95- * e.g. `prop = input()`. Returns null if it can't be parsed.
96- */
97- function parseTopLevelCall (
98- call : ts . CallExpression , fnNames : InitializerApiFunction [ ] ,
99- importTracker : ImportedSymbolsTracker ) : StaticInitializerData | null {
100- const node = call . expression ;
94+ // Find if the `target` matches one of the expected APIs are are looking for.
95+ apiName = fnNames . find ( n => n === target ! . text ) ;
10196
102- if ( ! ts . isIdentifier ( node ) ) {
97+ // Ensure the call refers to the real API function from Angular core.
98+ if ( apiName === undefined ||
99+ ! isReferenceToInitializerApiFunction ( apiName , target , isCore , reflector ) ) {
103100 return null ;
104101 }
105102
106- return fnNames . some (
107- name => importTracker . isPotentialReferenceToNamedImport ( node , name , CORE_MODULE ) ) ?
108- { node, isRequired : false } :
109- null ;
103+ return {
104+ apiName,
105+ call,
106+ isRequired : true ,
107+ } ;
110108}
111109
112110/**
113- * Attempts to parse a top-level call to a required initializer,
114- * e.g. `prop = input.required()`. Returns null if it can't be parsed.
111+ * Extracts the identifier property target of a expression, supporting
112+ * one level deep property accesses.
113+ *
114+ * e.g. `input.required` will return `required`.
115+ * e.g. `input` will return `input`.
116+ *
115117 */
116- function parseTopLevelRequiredCall (
117- call : ts . CallExpression , fnNames : InitializerApiFunction [ ] ,
118- importTracker : ImportedSymbolsTracker ) : StaticInitializerData | null {
119- const node = call . expression ;
120-
121- if ( ! ts . isPropertyAccessExpression ( node ) || ! ts . isIdentifier ( node . expression ) ||
122- node . name . text !== 'required' ) {
123- return null ;
118+ function extractPropertyTarget ( node : ts . Expression ) : ts . Identifier | null {
119+ if ( ts . isPropertyAccessExpression ( node ) && ts . isIdentifier ( node . name ) ) {
120+ return node . name ;
121+ } else if ( ts . isIdentifier ( node ) ) {
122+ return node ;
124123 }
125-
126- const expression = node . expression ;
127- const matchesCoreApi = fnNames . some (
128- name => importTracker . isPotentialReferenceToNamedImport ( expression , name , CORE_MODULE ) ) ;
129-
130- return matchesCoreApi ? { node : expression , isRequired : true } : null ;
124+ return null ;
131125}
132126
133-
134127/**
135- * Attempts to parse a top-level call to a function referenced via a namespace import,
136- * e.g. `prop = core.input.required()`. Returns null if it can't be parsed .
128+ * Verifies that the given identifier resolves to the given initializer API
129+ * function expression from Angular core .
137130 */
138- function parseTopLevelCallFromNamespace (
139- call : ts . CallExpression , fnNames : InitializerApiFunction [ ] ,
140- importTracker : ImportedSymbolsTracker ) : StaticInitializerData | null {
141- const node = call . expression ;
142-
143- if ( ! ts . isPropertyAccessExpression ( node ) ) {
144- return null ;
145- }
146-
147- let apiReference : ts . Identifier | null = null ;
148- let isRequired = false ;
149-
150- // `prop = core.input()`
151- if ( ts . isIdentifier ( node . expression ) && ts . isIdentifier ( node . name ) &&
152- importTracker . isPotentialReferenceToNamespaceImport ( node . expression , CORE_MODULE ) ) {
153- apiReference = node . name ;
154- } else if (
155- // `prop = core.input.required()`
156- ts . isPropertyAccessExpression ( node . expression ) &&
157- ts . isIdentifier ( node . expression . expression ) && ts . isIdentifier ( node . expression . name ) &&
158- importTracker . isPotentialReferenceToNamespaceImport (
159- node . expression . expression , CORE_MODULE ) &&
160- node . name . text === 'required' ) {
161- apiReference = node . expression . name ;
162- isRequired = true ;
163- }
164-
165- if ( apiReference === null || ! ( fnNames as string [ ] ) . includes ( apiReference . text ) ) {
166- return null ;
131+ function isReferenceToInitializerApiFunction (
132+ functionName : InitializerApiFunction , target : ts . Identifier , isCore : boolean ,
133+ reflector : ReflectionHost ) : boolean {
134+ let targetImport : { name : string , from : string } | null = reflector . getImportOfIdentifier ( target ) ;
135+ if ( targetImport === null ) {
136+ if ( ! isCore ) {
137+ return false ;
138+ }
139+ // We are compiling the core module, where no import can be present.
140+ targetImport = { name : target . text , from : CORE_MODULE } ;
167141 }
168142
169- return { node : apiReference , isRequired } ;
143+ return targetImport . name === functionName && targetImport . from === CORE_MODULE ;
170144}
0 commit comments