@@ -6,14 +6,18 @@ import type {
66 NodeArray ,
77 SourceFile ,
88} from "./ast.ts" ;
9- import { createToken } from "./factory.generated.ts" ;
9+ import {
10+ createSyntaxList ,
11+ createToken ,
12+ } from "./factory.generated.ts" ;
1013import {
1114 isJSDocNodeKind ,
1215 isKeywordKind ,
1316 isPrivateIdentifier ,
1417 isPropertyNameLiteral ,
1518 isTokenKind ,
1619} from "./is.ts" ;
20+ import type { Scanner } from "./scanner.ts" ;
1721import {
1822 createScanner ,
1923 skipTrivia ,
@@ -616,6 +620,156 @@ function getOrCreateToken(sourceFile: SourceFile, kind: SyntaxKind, pos: number,
616620 return token ;
617621}
618622
623+ const emptyArray : readonly Node [ ] = [ ] ;
624+
625+ function assertHasRealPosition ( node : Node ) : void {
626+ if ( node . pos < 0 || node . end < 0 ) {
627+ throw new Error ( "Node without a real position cannot be scanned and thus has no token nodes - use forEachChild and collect the result if that's fine" ) ;
628+ }
629+ }
630+
631+ export function getChildren ( node : Node , sourceFile : SourceFile = node . getSourceFile ( ) ) : readonly Node [ ] {
632+ // A SyntaxList already holds its (pre-materialized) children.
633+ if ( node . kind === SyntaxKind . SyntaxList ) {
634+ return ( node as unknown as { children : readonly Node [ ] ; } ) . children ;
635+ }
636+
637+ if ( isTokenKind ( node . kind ) ) {
638+ // EndOfFile may carry leading JSDoc; every other token has no children. The EndOfFile
639+ // result must go through the cache: remote nodes rebuild .jsDoc on every access.
640+ if ( node . kind !== SyntaxKind . EndOfFile ) {
641+ return emptyArray ;
642+ }
643+ }
644+ else {
645+ assertHasRealPosition ( node ) ;
646+ }
647+
648+ const cache = ( sourceFile . childrenCache ??= new Map < Node , readonly Node [ ] > ( ) ) ;
649+ const cached = cache . get ( node ) ;
650+
651+ if ( cached !== undefined ) {
652+ return cached ;
653+ }
654+
655+ const children = node . kind === SyntaxKind . EndOfFile
656+ ? node . jsDoc ?? emptyArray
657+ : createChildren ( node , sourceFile ) ;
658+ cache . set ( node , children ) ;
659+ return children ;
660+ }
661+
662+ function createChildren ( node : Node , sourceFile : SourceFile ) : readonly Node [ ] {
663+ const children : Node [ ] = [ ] ;
664+
665+ // Inside a JSDoc comment there are no real tokens to synthesize.
666+ if ( shouldSkipChild ( node ) ) {
667+ node . forEachChild ( child => void children . push ( child ) ) ;
668+ return children ;
669+ }
670+
671+ // One scanner serves every run of synthetic tokens materialized for this node.
672+ const scanner = createScanner ( /*skipTrivia*/ true , sourceFile . languageVariant , sourceFile . text ) ;
673+ let pos = node . pos ;
674+ const processNode = ( child : Node ) : undefined => {
675+ // Reparsed subtrees (JSDoc types materialized into the AST) have positions inside
676+ // the comment, not in this node's token range.
677+ if ( child . flags & NodeFlags . Reparsed ) {
678+ return ;
679+ }
680+ addSyntheticNodes ( children , pos , child . pos , node , sourceFile , scanner ) ;
681+ children . push ( child ) ;
682+ pos = child . end ;
683+ } ;
684+ const processNodes = ( nodes : NodeArray < Node > ) : undefined => {
685+ if ( nodes . length > 0 && ! nodes . some ( child => ! ( child . flags & NodeFlags . Reparsed ) ) ) {
686+ return ;
687+ }
688+ addSyntheticNodes ( children , pos , nodes . pos , node , sourceFile , scanner ) ;
689+ children . push ( createSyntaxListNode ( nodes , node , sourceFile , scanner ) ) ;
690+ pos = nodes . end ;
691+ } ;
692+
693+ // JSDoc attached to the node is leading content, processed first.
694+ if ( node . jsDoc ) {
695+ for ( const jsDoc of node . jsDoc ) {
696+ processNode ( jsDoc ) ;
697+ }
698+ }
699+ pos = node . pos ;
700+ node . forEachChild ( processNode , processNodes ) ;
701+ addSyntheticNodes ( children , pos , node . end , node , sourceFile , scanner ) ;
702+ return children ;
703+ }
704+
705+ function addSyntheticNodes ( children : Node [ ] , pos : number , end : number , parent : Node , sourceFile : SourceFile , scanner : Scanner ) : void {
706+ if ( pos >= end ) {
707+ return ;
708+ }
709+ scanner . resetTokenState ( pos ) ;
710+ scanner . scan ( ) ;
711+ while ( pos < end ) {
712+ const token = scanner . getToken ( ) ;
713+ const tokenEnd = scanner . getTokenEnd ( ) ;
714+ if ( tokenEnd <= end ) {
715+ // An identifier should never appear as trivia between AST children; skip defensively.
716+ if ( token !== SyntaxKind . Identifier ) {
717+ children . push ( getOrCreateToken ( sourceFile , token , pos , tokenEnd , parent , scanner . getTokenFlags ( ) ) ) ;
718+ }
719+ }
720+ pos = tokenEnd ;
721+ if ( token === SyntaxKind . EndOfFile ) {
722+ break ;
723+ }
724+ scanner . scan ( ) ;
725+ }
726+ }
727+
728+ function createSyntaxListNode ( nodes : NodeArray < Node > , parent : Node , sourceFile : SourceFile , scanner : Scanner ) : Node {
729+ const listChildren : Node [ ] = [ ] ;
730+ let pos = nodes . pos ;
731+ for ( const child of nodes ) {
732+ if ( child . flags & NodeFlags . Reparsed ) {
733+ continue ;
734+ }
735+ addSyntheticNodes ( listChildren , pos , child . pos , parent , sourceFile , scanner ) ;
736+ listChildren . push ( child ) ;
737+ pos = child . end ;
738+ }
739+ addSyntheticNodes ( listChildren , pos , nodes . end , parent , sourceFile , scanner ) ;
740+ const list = createSyntaxList ( listChildren ) as Mutable < Node > ;
741+ list . pos = nodes . pos ;
742+ list . end = nodes . end ;
743+ list . parent = parent ;
744+ return list as Node ;
745+ }
746+
747+ export function getFirstToken ( node : Node , sourceFile : SourceFile = node . getSourceFile ( ) ) : Node | undefined {
748+ if ( isTokenKind ( node . kind ) ) {
749+ return undefined ;
750+ }
751+ assertHasRealPosition ( node ) ;
752+ const children = getChildren ( node , sourceFile ) ;
753+ const child = children . find ( kid => kid . kind < SyntaxKind . FirstJSDocNode || kid . kind > SyntaxKind . LastJSDocNode ) ;
754+ if ( child === undefined ) {
755+ return undefined ;
756+ }
757+ return child . kind < SyntaxKind . FirstNode ? child : getFirstToken ( child , sourceFile ) ;
758+ }
759+
760+ export function getLastToken ( node : Node , sourceFile : SourceFile = node . getSourceFile ( ) ) : Node | undefined {
761+ if ( isTokenKind ( node . kind ) ) {
762+ return undefined ;
763+ }
764+ assertHasRealPosition ( node ) ;
765+ const children = getChildren ( node , sourceFile ) ;
766+ const child = children . length ? children [ children . length - 1 ] : undefined ;
767+ if ( child === undefined ) {
768+ return undefined ;
769+ }
770+ return child . kind < SyntaxKind . FirstNode ? child : getLastToken ( child , sourceFile ) ;
771+ }
772+
619773/** Binary search a node list for the node containing position. */
620774function binarySearchNodeList (
621775 nodes : NodeArray < Node > ,
0 commit comments