import { } from 'ts-expose-internals' ; import { createGenerator , GeneratorContext } from '@crosstype/build-tools' ; import path from 'path' ; import ts , { CompilerHost , InterfaceDeclaration , InterfaceType , Program , PropertyAssignment , QualifiedName , ScriptTarget , SourceFile , SyntaxKind , Type , TypeFlags , TypeNode } from 'typescript' ; import { PluginConfig , ProgramTransformerExtras } from 'ts-patch' ; import { tsquery } from '@phenomnomnominal/tsquery' ; import { getAllBaseTypes , getPrimaryDeclaration } from '@crosstype/common' ; const targetFile = path . resolve ( __dirname , '../src/ast/node-types.ts' ) ; const baseNodeInterfaceName = 'Node' ; const namedNodeInterfaceName = 'NamedNode' ; const flagsHelperName = 'Flags' ; const lookupHelperName = 'NodeForKind' ; const outFiles = { nodeLookups : path . join ( __dirname , '../generated/ast/node-lookups.ts' ) , nodeAliases : path . join ( __dirname , '../generated/ast/node-aliases.ts' ) , nodeMetadata : path . join ( __dirname , '../generated/ast/node-metadata.ts' ) , } ; const selectors = { interfaces : `SourceFile > InterfaceDeclaration` } ; interface ASTDetail { baseNodeInterface : InterfaceDeclaration ; baseNodeInterfaceType : Type ; namedNodeInterfaceType : Type ; astFile : SourceFile ; nodeInterfaces : Map < InterfaceDeclaration , { typeFlags : string [ ] ; flags : string [ ] ; } > ; interfaceDeclarations : InterfaceDeclaration [ ] ; } function getASTDetail ( program ) { const checker = program . getTypeChecker ( ) ; const astFile = program . getSourceFile ( targetFile ) ; const interfaceDeclarations = tsquery . query ( astFile , selectors . interfaces ) as InterfaceDeclaration [ ] ; const baseNodeInterface = interfaceDeclarations . find ( node => node . name . text === baseNodeInterfaceName ) ! ; const baseNodeInterfaceType = checker . getTypeAtLocation ( baseNodeInterface ) ! ; const namedNodeInterface = interfaceDeclarations . find ( node => node . name . text === namedNodeInterfaceName ) ! ; const namedNodeInterfaceType = checker . getTypeAtLocation ( namedNodeInterface ) ! ; const nodeInterfaces = new Map < InterfaceDeclaration , { typeFlags : string [ ] ; flags : string [ ] ; } > ( ) ; for ( const node of interfaceDeclarations ) getInterfaceNodes ( node ) ; for ( const node of nodeInterfaces . keys ( ) ) if ( ! node . members ?. some ( m => ts . isPropertySignature ( m ) && ( < any > m ) . name . text === 'kind' ) ) nodeInterfaces . delete ( node ) ; return ( { interfaceDeclarations , baseNodeInterface , baseNodeInterfaceType , namedNodeInterfaceType , nodeInterfaces , astFile } ) ; function getInterfaceNodes ( declaration ) { const isNodeBasedInterface = ( declaration ) : boolean => ( declaration !== baseNodeInterface ) && checker . isTypeAssignableTo ( checker . getTypeAtLocation ( declaration ) , baseNodeInterfaceType ) ; const res = { typeFlags : [ ] , flags : [ ] } ; const addFlags = ( kind , flags ) => { if ( ! flags ) return ; const target = ( kind === 'type' ) ? 'typeFlags' : 'flags' ; if ( typeof flags === 'string' ) res [ target ] . push ( ... flags . split ( /\s*\|\s*/ ) ) ; else res [ target ] . push ( ... flags ) ; } ; if ( nodeInterfaces . has ( declaration ) ) return nodeInterfaces . get ( declaration ) ; if ( ! isNodeBasedInterface ( declaration ) ) return ; const heritageTypes = declaration . heritageClauses ?. [ 0 ] ?. types ; if ( ! heritageTypes || ! heritageTypes . length ) return ; for ( const node of heritageTypes ) { if ( ts . isExpressionWithTypeArguments ( node ) && ts . isIdentifier ( node . expression ) && node . expression . text === flagsHelperName ) { const flagGroup = node . typeArguments ! [ 0 ] . getText ( ) ; const flagTags = node . typeArguments ! [ 1 ] . getText ( ) ; const newFlags = flagTags . replace ( /['"](\w+)['"]/g , `${ flagGroup }.$1` ) ; addFlags ( flagGroup === 'NodeFlags' ? 'node' : 'type' , newFlags ) ; continue ; } const type = checker . getTypeAtLocation ( node ) ; const typeDeclaration = getPrimaryDeclaration ( type ) ; if ( ts . isInterfaceDeclaration ( typeDeclaration ) && isNodeBasedInterface ( typeDeclaration ) ) { const subNodeFlags = getInterfaceNodes ( typeDeclaration ) ; addFlags ( 'type' , subNodeFlags ?. typeFlags ) ; addFlags ( 'node' , subNodeFlags ?. flags ) ; } } nodeInterfaces . set ( declaration , res ) ; return res ; } } const formatJSDocComment = ( msg , maxLength = 120 ) => { let res = '/**' ; const len = maxLength - 3 ; for ( let i = 0 ; i < msg . length ; i += len ) res += `\n * ${ msg . substr ( i , len ) }` ; return res + '\n */\n' ; } ; const createImportsDeclaration = ( names , from = '#ast' ) => ts . createImportDeclaration ( undefined , undefined , ts . createImportClause ( undefined , ts . createNamedImports ( names . map ( name => ts . createImportSpecifier ( undefined , ts . createIdentifier ( name ) ) ) ) , false ) , ts . createStringLiteral ( from ) ) ; /**
 * ts-patch Plugin entry point
 */ function entry ( this , program , host , config , extras ) { const tsInstance = extras . ts as unknown as typeof ts ; const checker = program . getTypeChecker ( ) ; const detail = getASTDetail ( program ) ; const printer = ts . createPrinter ( ) ; generateNodeLookups ( this ) ; generateNodeAliases ( this ) ; generateNodeMetadata ( this ) ; const compilerOptions = program . getCompilerOptions ( ) ; const rootFileNames = new Set ( program . getRootFileNames ( ) ) ; Object . values ( outFiles ) . forEach ( f => rootFileNames . add ( f ) ) ; return tsInstance . createProgram ( [ ... rootFileNames ] , compilerOptions , tsInstance . createIncrementalCompilerHost ( compilerOptions ) ) ; /**
     * Generates node-lookups.ts
     */ function generateNodeLookups ( context ) { const importDeclaration = createImportsDeclaration ( [ 'NodeKind' , ... [ ... detail . nodeInterfaces . keys ( ) ] . map ( node => node . name . text ) ] ) ; let typeNode = ts . createKeywordTypeNode ( ts . SyntaxKind . NeverKeyword ) ; for ( const node of [ ... detail . nodeInterfaces . keys ( ) ] . reverse ( ) ) { const qualifiedName = ( < any > node . members ) . find ( ( m ) => m . name . text === 'kind' ) ! . type . typeName as QualifiedName ; typeNode = ts . createConditionalTypeNode ( ts . createTypeReferenceNode ( ts . createIdentifier ( 'K' ) , undefined ) , ts . createTypeReferenceNode ( qualifiedName , undefined ) , ts . createTypeReferenceNode ( node . name , undefined ) , typeNode ) ; } const declaration = ts . createTypeAliasDeclaration ( undefined , [ ts . createModifier ( ts . SyntaxKind . ExportKeyword ) ] , ts . createIdentifier ( lookupHelperName ) , [ ts . createTypeParameterDeclaration ( ts . createIdentifier ( 'K' ) , ts . createTypeReferenceNode ( ts . createIdentifier ( 'NodeKind' ) , undefined ) , undefined ) ] , typeNode ) ; const sourceFile = ts . createSourceFile ( outFiles . nodeLookups , '' , ScriptTarget . ES2017 ) ; sourceFile . statements = ts . createNodeArray ( [ importDeclaration , declaration ] ) ; const sourceText = printer . printFile ( sourceFile ) . replace ( /(K extends [\w.]+? \? \w+ : )/g , `\n  $1` ) ; context . writeFile ( outFiles . nodeLookups , sourceText ) ; } /**
     * Generates node-aliases.ts
     */ function generateNodeAliases ( context ) { const createAliasDeclaration = ( name , nodes ) => ts . createTypeAliasDeclaration ( undefined , [ ts . createModifier ( ts . SyntaxKind . ExportKeyword ) ] , ts . createIdentifier ( name ) , undefined , ts . createUnionTypeNode ( nodes . map ( name => ts . createTypeReferenceNode ( name , undefined ) ) ) ) ; const nodeNames = { any : [ ] , nonValue : [ ] , value : [ ] , declaration : [ ] , rootDeclaration : [ ] } ; for ( const [ node , { typeFlags , flags } ] of detail . nodeInterfaces . entries ( ) ) { const name = node . name . text ; nodeNames . any . push ( name ) ; if ( typeFlags . includes ( 'TypeFlags.Module' ) || flags . includes ( 'NodeFlags.Nested' ) || flags . includes ( 'NodeFlags.Declaration' ) || flags . includes ( 'NodeFlags.Definition' ) ) nodeNames . nonValue . push ( name ) ; else nodeNames . value . push ( name ) ; if ( flags . includes ( 'NodeFlags.Declaration' ) ) { nodeNames . declaration . push ( name ) ; if ( ! flags . includes ( 'NodeFlags.Nested' ) ) nodeNames . rootDeclaration . push ( name ) ; } } const sourceFile = ts . createSourceFile ( outFiles . nodeAliases , '' , ScriptTarget . ES2017 ) ; sourceFile . statements = ts . createNodeArray ( [ createImportsDeclaration ( nodeNames . any ) , createAliasDeclaration ( 'AnyNode' , nodeNames . any ) , createAliasDeclaration ( 'ValueNode' , nodeNames . value ) , createAliasDeclaration ( 'NonValueNode' , nodeNames . nonValue ) , createAliasDeclaration ( 'Declaration' , nodeNames . declaration ) , createAliasDeclaration ( 'RootDeclaration' , nodeNames . rootDeclaration ) ] ) ; const sourceText = printer . printFile ( sourceFile ) . replace ( /(^export type AnyNode =)/m , formatJSDocComment ( `All valid nodes` ) + '$1' ) . replace ( /(^export type NonValueNode =)/m , formatJSDocComment ( `Nodes which cannot be used in the general value position (must have a specific parent)` ) + '$1' ) . replace ( /(^export type ValueNode =)/m , formatJSDocComment ( `Nodes that can be used in the general value position (can have any parent)` ) + '$1' ) ; context . writeFile ( outFiles . nodeAliases , sourceText ) ; } /**
     * Generates node-metadata.ts
     */ function generateNodeMetadata ( context ) { const isValidChildContainerType = ( type ) => ( checker . isTypeAssignableTo ( type , detail . baseNodeInterfaceType ) ) || ( [ 'NodeMap' , 'NodeSet' ] . includes ( type . symbol ?. getEscapedName ( ) . toString ( ) ) ) ; const propertyAssignments = [ ... detail . nodeInterfaces . entries ( ) ] . map ( ( [ node , { typeFlags , flags } ] ) => { const nodeKindName = ( < any > node . members ) . find ( ( m ) => m . name . text === 'kind' ) ! . type . typeName as QualifiedName ; const childContainers = [ ] ; const nodeType = checker . getTypeAtLocation ( node ) ; for ( const propSymbol of nodeType . getProperties ( ) ) { const m = propSymbol . valueDeclaration ; if ( ! ts . isPropertySignature ( m ) || ! m . type ) continue ; const name = m . name . getText ( ) ; const type = checker . getTypeAtLocation ( m . type ) ; const isReadonly = ! ! m . modifiers ?. some ( modifier => modifier . kind === SyntaxKind . ReadonlyKeyword ) ; const isOptional = ! ! m . questionToken ; if ( m . symbol . getJsDocTags ( ) . some ( t => t . name . toLowerCase ( ) === 'notchild' ) || isReadonly ) continue ; if ( ( type . isUnion ( ) && type . types . every ( isValidChildContainerType ) ) || isValidChildContainerType ( type ) ) childContainers . push ( { key : name , optional : isOptional } ) ; } const objectPropertes = [ ] ; if ( childContainers . length ) objectPropertes . push ( ts . createPropertyAssignment ( ts . createIdentifier ( 'childContainerProperties' ) , ts . createNew ( ts . createIdentifier ( 'Map' ) , undefined , [ ts . createArrayLiteral ( childContainers . map ( c => ts . createArrayLiteral ( [ ts . createStringLiteral ( c . key ) , ts . createObjectLiteral ( [ ts . createPropertyAssignment ( ts . createIdentifier ( 'key' ) , ts . createStringLiteral ( c . key ) ) , ts . createPropertyAssignment ( ts . createIdentifier ( 'optional' ) , c . optional ? ts . createTrue ( ) : ts . createFalse ( ) ) ] , false ) ] ) ) , true ) ] ) ) ) ; if ( typeFlags . length ) objectPropertes . push ( ts . createPropertyAssignment ( ts . createIdentifier ( 'baseTypeFlags' ) , ts . createArrayLiteral ( typeFlags . map ( f => ts . createIdentifier ( f ) ) ) ) ) ; if ( flags . length ) objectPropertes . push ( ts . createPropertyAssignment ( ts . createIdentifier ( 'baseFlags' ) , ts . createArrayLiteral ( flags . map ( f => ts . createIdentifier ( f ) ) ) ) ) ; if ( getAllBaseTypes ( < InterfaceType > nodeType ) . find ( t => ( < any > t ) . target === detail . namedNodeInterfaceType ) ) objectPropertes . push ( ts . createPropertyAssignment ( ts . createIdentifier ( 'isNamedNode' ) , ts . createTrue ( ) ) ) ; return ts . createPropertyAssignment ( ts . createComputedPropertyName ( ts . createPropertyAccess ( ts . createIdentifier ( nodeKindName . left . getText ( ) ) , nodeKindName . right ) ) , ts . createObjectLiteral ( objectPropertes , true ) ) ; } ) ; const metadataStatement = ts . createVariableStatement ( [ ts . createModifier ( ts . SyntaxKind . ExportKeyword ) ] , ts . createVariableDeclarationList ( [ ts . createVariableDeclaration ( ts . createIdentifier ( 'nodeMetadata' ) , ts . createTypeReferenceNode ( ts . createIdentifier ( 'Record' ) , [ ts . createTypeReferenceNode ( ts . createIdentifier ( 'NodeKind' ) , undefined ) , ts . createTypeReferenceNode ( ts . createIdentifier ( 'NodeMetadata' ) , undefined ) ] ) , ts . createObjectLiteral ( propertyAssignments , true ) ) ] , ts . NodeFlags . Const ) ) ; const sourceFile = ts . createSourceFile ( outFiles . nodeAliases , '' , ScriptTarget . ES2017 ) ; sourceFile . statements = ts . createNodeArray ( [ createImportsDeclaration ( [ 'NodeKind' , 'NodeMetadata' , 'NodeFlags' , 'TypeFlags' ] ) , metadataStatement ] ) ; const sourceText = printer . printFile ( sourceFile ) ; context . writeFile ( outFiles . nodeMetadata , sourceText ) ; } } const generator = createGenerator ( { name : 'AST' , watchFiles : [ path . join ( __dirname , '../src/ast/node-types.ts' ) ] , outFiles : Object . values ( outFiles ) , entry } ) ; export default generator ;