11package main
22
33import (
4+ "context"
5+ "encoding/json"
6+ "errors"
47 "fmt"
58 "os"
69 "sort"
@@ -16,6 +19,7 @@ import (
1619 "github.com/microsoft/typescript-go/shim/vfs/osvfs"
1720 api "github.com/web-infra-dev/rslint/internal/api"
1821 rslintconfig "github.com/web-infra-dev/rslint/internal/config"
22+ "github.com/web-infra-dev/rslint/internal/inspector"
1923 "github.com/web-infra-dev/rslint/internal/linter"
2024 "github.com/web-infra-dev/rslint/internal/rule"
2125 "github.com/web-infra-dev/rslint/internal/utils"
@@ -24,6 +28,18 @@ import (
2428// IPCHandler implements the ipc.Handler interface
2529type IPCHandler struct {}
2630
31+ // programCache holds a cached Program instance for AST info requests
32+ type programCache struct {
33+ mu sync.RWMutex
34+ fileContent string
35+ compilerOptions string // JSON serialized for comparison
36+ program * compiler.Program
37+ sourceFile * ast.SourceFile
38+ }
39+
40+ // Global program cache for AST info requests
41+ var astInfoProgramCache = & programCache {}
42+
2743// HandleLint handles lint requests in IPC mode
2844func (h * IPCHandler ) HandleLint (req api.LintRequest ) (* api.LintResponse , error ) {
2945
@@ -328,6 +344,191 @@ func (h *IPCHandler) HandleApplyFixes(req api.ApplyFixesRequest) (*api.ApplyFixe
328344 }, nil
329345}
330346
347+ // HandleGetAstInfo handles get AST info requests in IPC mode
348+ func (h * IPCHandler ) HandleGetAstInfo (req api.GetAstInfoRequest ) (* api.GetAstInfoResponse , error ) {
349+ // Fixed user file name for program creation
350+ const userFileName = "/index.ts"
351+
352+ // Serialize compiler options for comparison
353+ compilerOptionsJSON := "{}"
354+ if req .CompilerOptions != nil {
355+ jsonBytes , err := json .Marshal (req .CompilerOptions )
356+ if err != nil {
357+ return nil , fmt .Errorf ("failed to marshal compiler options: %w" , err )
358+ }
359+ compilerOptionsJSON = string (jsonBytes )
360+ }
361+
362+ // Check if we can use cached program
363+ program , userSourceFile := getCachedProgram (req .FileContent , compilerOptionsJSON )
364+ if program == nil || userSourceFile == nil {
365+ // Cache miss - create new program
366+ var err error
367+ program , userSourceFile , err = createAndCacheProgram (userFileName , req .FileContent , compilerOptionsJSON , req .CompilerOptions )
368+ if err != nil {
369+ return nil , err
370+ }
371+ }
372+
373+ // Get type checker
374+ typeChecker , done := program .GetTypeChecker (context .Background ())
375+ defer done ()
376+
377+ // Determine which source file to query
378+ // If FileName is set to an external file, query that file (e.g., lib.d.ts)
379+ // Otherwise, query the user's source file
380+ var targetSourceFile * ast.SourceFile
381+ if req .FileName != "" && req .FileName != userFileName {
382+ targetSourceFile = program .GetSourceFile (req .FileName )
383+ if targetSourceFile == nil {
384+ return & api.GetAstInfoResponse {}, nil
385+ }
386+ } else {
387+ targetSourceFile = userSourceFile
388+ }
389+
390+ isExternalFile := targetSourceFile != userSourceFile
391+
392+ // Build the response
393+ // Use userSourceFile as the "current" file for the builder
394+ // This determines which files are considered "external" (fileName will be set for nodes not in userSourceFile)
395+ builder := api .NewAstInfoBuilder (typeChecker , userSourceFile )
396+ response := & api.GetAstInfoResponse {}
397+
398+ // Special case: if requesting SourceFile by kind, build it directly without Node conversion
399+ if req .Kind > 0 && ast .Kind (req .Kind ) == ast .KindSourceFile {
400+ response .Node = builder .BuildSourceFileNodeInfo (targetSourceFile )
401+ // SourceFile doesn't have type/symbol/signature/flow, so return early
402+ return response , nil
403+ }
404+
405+ // Find the node at the specified position (with optional end for exact matching)
406+ node := inspector .FindNodeAtPosition (targetSourceFile , req .Position , req .End , req .Kind )
407+ if node == nil {
408+ return & api.GetAstInfoResponse {}, nil
409+ }
410+
411+ // Build node info
412+ response .Node = builder .BuildNodeInfo (node )
413+
414+ // Build type info
415+ t := inspector .GetTypeAtNode (typeChecker , node )
416+ if t != nil {
417+ response .Type = builder .BuildTypeInfo (t )
418+ }
419+
420+ // Build symbol info
421+ // First try to get symbol directly from node
422+ symbol := typeChecker .GetSymbolAtLocation (node )
423+ // If no symbol at node, try to get it from the type
424+ if symbol == nil && t != nil {
425+ symbol = t .Symbol ()
426+ }
427+ if symbol != nil {
428+ response .Symbol = builder .BuildSymbolInfo (symbol )
429+ }
430+
431+ // Build signature info
432+ sig := inspector .GetSignatureOfNode (typeChecker , node )
433+ if sig != nil {
434+ response .Signature = builder .BuildSignatureInfo (sig )
435+ }
436+
437+ // Build flow info (only for nodes in user's source file)
438+ if ! isExternalFile {
439+ flowNode := inspector .GetFlowNodeOfNode (node )
440+ if flowNode != nil {
441+ response .Flow = builder .BuildFlowInfo (flowNode )
442+ }
443+ }
444+
445+ return response , nil
446+ }
447+
448+ // getCachedProgram returns the cached program if it matches the current request
449+ func getCachedProgram (fileContent , compilerOptionsJSON string ) (* compiler.Program , * ast.SourceFile ) {
450+ astInfoProgramCache .mu .RLock ()
451+ defer astInfoProgramCache .mu .RUnlock ()
452+
453+ if astInfoProgramCache .program == nil {
454+ return nil , nil
455+ }
456+
457+ // Check if cache is valid (only fileContent and compilerOptions matter)
458+ if astInfoProgramCache .fileContent == fileContent &&
459+ astInfoProgramCache .compilerOptions == compilerOptionsJSON {
460+ return astInfoProgramCache .program , astInfoProgramCache .sourceFile
461+ }
462+
463+ return nil , nil
464+ }
465+
466+ // createAndCacheProgram creates a new program and caches it
467+ func createAndCacheProgram (fileName , fileContent , compilerOptionsJSON string , compilerOptions map [string ]any ) (* compiler.Program , * ast.SourceFile , error ) {
468+ // Create a virtual filesystem with the provided file content
469+ fs := bundled .WrapFS (cachedvfs .From (osvfs .FS ()))
470+
471+ fileContents := map [string ]string {
472+ fileName : fileContent ,
473+ }
474+ fs = utils .NewOverlayVFS (fs , fileContents )
475+
476+ // Build tsconfig from request options or use defaults
477+ tsconfigContent := buildTsConfigContent (fileName , compilerOptions )
478+ tsconfigPath := "/tsconfig.json"
479+ fs = utils .NewOverlayVFS (fs , map [string ]string {
480+ tsconfigPath : tsconfigContent ,
481+ })
482+
483+ // Create compiler host and program
484+ host := utils .CreateCompilerHost ("/" , fs )
485+ program , err := utils .CreateProgram (false , fs , "/" , tsconfigPath , host )
486+ if err != nil {
487+ return nil , nil , fmt .Errorf ("failed to create program: %w" , err )
488+ }
489+
490+ // Get the source file
491+ sourceFile := program .GetSourceFile (fileName )
492+ if sourceFile == nil {
493+ return nil , nil , errors .New ("failed to get source file" )
494+ }
495+
496+ // Update cache
497+ astInfoProgramCache .mu .Lock ()
498+ astInfoProgramCache .fileContent = fileContent
499+ astInfoProgramCache .compilerOptions = compilerOptionsJSON
500+ astInfoProgramCache .program = program
501+ astInfoProgramCache .sourceFile = sourceFile
502+ astInfoProgramCache .mu .Unlock ()
503+
504+ return program , sourceFile , nil
505+ }
506+
507+ // buildTsConfigContent creates a tsconfig.json content string from compiler options
508+ func buildTsConfigContent (fileName string , compilerOptions map [string ]any ) string {
509+ // Default compiler options
510+ opts := map [string ]any {
511+ "target" : "ESNext" ,
512+ "module" : "ESNext" ,
513+ "strict" : true ,
514+ "strictNullChecks" : true ,
515+ }
516+
517+ // Merge with provided options (provided options override defaults)
518+ for k , v := range compilerOptions {
519+ opts [k ] = v
520+ }
521+
522+ // Serialize compiler options to JSON
523+ optsJSON , err := json .Marshal (opts )
524+ if err != nil {
525+ // Fallback to minimal config on error
526+ return fmt .Sprintf (`{"compilerOptions":{"target":"ESNext","module":"ESNext","strict":true},"files":["%s"]}` , fileName )
527+ }
528+
529+ return fmt .Sprintf (`{"compilerOptions":%s,"files":["%s"]}` , string (optsJSON ), fileName )
530+ }
531+
331532// runAPI runs the linter in IPC mode
332533func runAPI () int {
333534 handler := & IPCHandler {}
0 commit comments