@@ -66,6 +66,7 @@ import {
6666 findAllMatchingNodes ,
6767 findFirstMatchingNode ,
6868 hasExpressionIdentifier ,
69+ readDirectiveIdFromComment ,
6970} from './comments' ;
7071import { TypeCheckData } from './context' ;
7172import { isAccessExpression , isDirectiveDeclaration } from './ts_util' ;
@@ -207,36 +208,52 @@ export class SymbolBuilder {
207208 templateNode : TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective ,
208209 ) : DirectiveSymbol [ ] {
209210 const elementSourceSpan = templateNode . startSourceSpan ?? templateNode . sourceSpan ;
210- const nodes = findAllMatchingNodes ( this . typeCheckBlock , {
211- withSpan : elementSourceSpan ,
212- filter : isDirectiveDeclaration ,
213- } ) ;
214- const symbols : DirectiveSymbol [ ] = [ ] ;
215- const seenDirectives = new Set < ts . ClassDeclaration > ( ) ;
211+ const boundDirectives = this . typeCheckData . boundTarget . getDirectivesOfNode ( templateNode ) ?? [ ] ;
216212
217- let boundDirectives = this . typeCheckData . boundTarget . getDirectivesOfNode ( templateNode ) ?? [ ] ;
213+ let symbols = this . getDirectiveSymbolsForDirectives ( boundDirectives , elementSourceSpan ) ;
218214
219215 // 'getDirectivesOfNode' will not return the directives intended for an element
220216 // on a microsyntax template, for example '<div *ngFor="let user of users;" dir>',
221217 // the 'dir' will be skipped, but it's needed in language service.
222218 if ( ! ( templateNode instanceof TmplAstDirective ) ) {
223- const firstChild = templateNode . children ?. [ 0 ] ;
224- if ( firstChild instanceof TmplAstElement ) {
219+ const firstChild = templateNode . children . find (
220+ ( c ) : c is TmplAstElement => c instanceof TmplAstElement ,
221+ ) ;
222+ if ( firstChild !== undefined ) {
225223 const isMicrosyntaxTemplate =
226224 templateNode instanceof TmplAstTemplate &&
227225 sourceSpanEqual ( firstChild . sourceSpan , templateNode . sourceSpan ) ;
228226 if ( isMicrosyntaxTemplate ) {
229227 const firstChildDirectives =
230228 this . typeCheckData . boundTarget . getDirectivesOfNode ( firstChild ) ;
231- if ( firstChildDirectives !== null && boundDirectives . length > 0 ) {
232- boundDirectives = boundDirectives . concat ( firstChildDirectives ) ;
233- } else if ( firstChildDirectives !== null ) {
234- boundDirectives = firstChildDirectives ;
229+ if ( firstChildDirectives !== null ) {
230+ const childSymbols = this . getDirectiveSymbolsForDirectives (
231+ firstChildDirectives ,
232+ elementSourceSpan ,
233+ ) ;
234+ // Merge symbols, avoiding duplicates
235+ for ( const symbol of childSymbols ) {
236+ if ( ! symbols . some ( ( s ) => s . ref . node === symbol . ref . node ) ) {
237+ symbols . push ( symbol ) ;
238+ }
239+ }
235240 }
236241 }
237242 }
238243 }
239244
245+ return symbols ;
246+ }
247+
248+ private getDirectiveSymbolsForDirectives (
249+ boundDirectives : TypeCheckableDirectiveMeta [ ] ,
250+ span : ParseSourceSpan ,
251+ ) : DirectiveSymbol [ ] {
252+ const nodes = findAllMatchingNodes ( this . typeCheckBlock , {
253+ withSpan : span ,
254+ filter : isDirectiveDeclaration ,
255+ } ) ;
256+
240257 const hostDirectiveMap = new Map < ts . Node , HostDirectiveMeta > ( ) ;
241258 for ( const d of boundDirectives ) {
242259 if ( d . hostDirectives ) {
@@ -248,33 +265,14 @@ export class SymbolBuilder {
248265 }
249266 }
250267
251- for ( let i = 0 ; i < nodes . length ; i ++ ) {
252- const node = nodes [ i ] ;
253-
254- let nodeName : string | null = null ;
255- let typeNode = ts . isTypeNode ( node )
256- ? node
257- : ts . isIdentifier ( node ) && node . parent && ts . isVariableDeclaration ( node . parent )
258- ? node . parent . type
259- : null ;
260- if ( typeNode && ts . isTypeReferenceNode ( typeNode ) ) {
261- const typeName = typeNode . typeName ;
262- nodeName = ts . isIdentifier ( typeName ) ? typeName . text : typeName . right . text ;
263- } else if ( typeNode && ts . isIntersectionTypeNode ( typeNode ) ) {
264- const first = typeNode . types [ 0 ] ;
265- if ( ts . isTypeReferenceNode ( first ) ) {
266- const typeName = first . typeName ;
267- nodeName = ts . isIdentifier ( typeName ) ? typeName . text : typeName . right . text ;
268- }
269- }
270-
271- // Match by name with index fallback
272- let meta = boundDirectives [ i ] ;
273- if ( nodeName ) {
274- meta =
275- boundDirectives . find ( ( m ) => m . ref . node . name && m . ref . node . name . text === nodeName ) ?? meta ;
276- }
268+ const symbols : DirectiveSymbol [ ] = [ ] ;
269+ const seenDirectives = new Set < ts . ClassDeclaration > ( ) ;
270+ const sf = this . typeCheckBlock . getSourceFile ( ) ;
277271
272+ for ( const node of nodes ) {
273+ const id = readDirectiveIdFromComment ( sf , node ) ;
274+ if ( id === null ) continue ;
275+ const meta = boundDirectives [ id ] ;
278276 if ( ! meta ) continue ;
279277
280278 const declaration = meta . ref . node as unknown as ts . ClassDeclaration ;
@@ -316,85 +314,9 @@ export class SymbolBuilder {
316314 }
317315 }
318316
319- // Sort to ensure host directives appear first (matching test expectations)
320- symbols . sort ( ( a , b ) => {
321- if ( a . matchSource === MatchSource . HostDirective && b . matchSource === MatchSource . Selector ) {
322- return - 1 ;
323- }
324- if ( a . matchSource === MatchSource . Selector && b . matchSource === MatchSource . HostDirective ) {
325- return 1 ;
326- }
327- return 0 ;
328- } ) ;
329-
330317 return symbols ;
331318 }
332319
333- private getDirectiveMeta (
334- host : TmplAstTemplate | TmplAstElement | TmplAstComponent | TmplAstDirective ,
335- directiveDeclaration : ts . ClassDeclaration ,
336- ) : TypeCheckableDirectiveMeta | null {
337- let directives = this . typeCheckData . boundTarget . getDirectivesOfNode ( host ) ;
338-
339- // `getDirectivesOfNode` will not return the directives intended for an element
340- // on a microsyntax template, for example `<div *ngFor="let user of users;" dir>`,
341- // the `dir` will be skipped, but it's needed in language service.
342- if ( ! ( host instanceof TmplAstDirective ) ) {
343- const firstChild = host . children [ 0 ] ;
344- if ( firstChild instanceof TmplAstElement ) {
345- const isMicrosyntaxTemplate =
346- host instanceof TmplAstTemplate &&
347- sourceSpanEqual ( firstChild . sourceSpan , host . sourceSpan ) ;
348- if ( isMicrosyntaxTemplate ) {
349- const firstChildDirectives =
350- this . typeCheckData . boundTarget . getDirectivesOfNode ( firstChild ) ;
351- if ( firstChildDirectives !== null && directives !== null ) {
352- directives = directives . concat ( firstChildDirectives ) ;
353- } else {
354- directives = directives ?? firstChildDirectives ;
355- }
356- }
357- }
358- }
359- if ( directives === null ) {
360- return null ;
361- }
362-
363- const directive = directives . find ( ( m ) =>
364- isSameDirectiveDeclaration ( m . ref . node , directiveDeclaration ) ,
365- ) ;
366- if ( directive ) {
367- return directive ;
368- }
369-
370- const originalFile = ( directiveDeclaration . getSourceFile ( ) as MaybeSourceFileWithOriginalFile ) [
371- NgOriginalFile
372- ] ;
373-
374- if ( originalFile !== undefined ) {
375- // This is a preliminary check ahead of a more expensive search
376- const hasPotentialCandidate = directives . find (
377- ( m ) => m . ref . node . name . text === directiveDeclaration . name ?. text ,
378- ) ;
379-
380- if ( hasPotentialCandidate ) {
381- // In case the TCB has been inlined,
382- // We will look for a matching class
383- // If we find one, we look for it in the directives array
384- const classWithSameName = findMatchingDirective ( originalFile , directiveDeclaration ) ;
385- if ( classWithSameName !== null ) {
386- return (
387- directives . find ( ( m ) => isSameDirectiveDeclaration ( m . ref . node , classWithSameName ) ) ??
388- null
389- ) ;
390- }
391- }
392- }
393-
394- // Really nothing was found
395- return null ;
396- }
397-
398320 private getDirectiveModule ( declaration : ts . ClassDeclaration ) : ClassDeclaration | null {
399321 const scope = this . componentScopeReader . getScopeForComponent ( declaration as ClassDeclaration ) ;
400322 if ( scope === null || scope . kind !== ComponentScopeKind . NgModule ) {
0 commit comments