( projectPath: string, extraExtensions?: Set<string>, progress?: GraphBuildProgress, )
| 642 | * `symbolsByFile` / `outgoingCallsByFile` and persisted by `doRebuildGraph`. |
| 643 | */ |
| 644 | export async function buildCodeGraph( |
| 645 | projectPath: string, |
| 646 | extraExtensions?: Set<string>, |
| 647 | progress?: GraphBuildProgress, |
| 648 | ): Promise<CodeGraph & { |
| 649 | symbolsByFile: Map<string, SymbolNode[]>; |
| 650 | outgoingCallsByFile: Map<string, SymbolEdge[]>; |
| 651 | }> { |
| 652 | ensureDynamicLanguages(); |
| 653 | |
| 654 | const resolvedPath = path.resolve(projectPath); |
| 655 | const aliases = await loadPathAliases(resolvedPath); |
| 656 | const files = await getGraphableFiles(resolvedPath, extraExtensions); |
| 657 | const fileSet = new Set(files); |
| 658 | |
| 659 | if (progress) { |
| 660 | progress.filesTotal = files.length; |
| 661 | progress.phase = "analyzing imports"; |
| 662 | } |
| 663 | |
| 664 | logger.info("Building code graph", { projectPath: resolvedPath, fileCount: files.length }); |
| 665 | |
| 666 | const nodesMap = new Map<string, CodeGraphNode>(); |
| 667 | const edges: CodeGraphEdge[] = []; |
| 668 | const symbolsByFile = new Map<string, SymbolNode[]>(); |
| 669 | const outgoingCallsByFile = new Map<string, SymbolEdge[]>(); |
| 670 | |
| 671 | // Build a suffix lookup map for JVM multi-module projects (Java/Kotlin/Scala). |
| 672 | // This resolves FQNs like com.example.Foo when the class lives under a nested |
| 673 | // src/main/java/ tree (e.g. module-a/sub/src/main/java/com/example/Foo.java). |
| 674 | // Cost: O(n) once here, O(1) per import lookup (negligible vs. full AST parse). |
| 675 | const hasJvm = files.some((f) => { |
| 676 | const e = path.extname(f).toLowerCase(); |
| 677 | return e === ".java" || e === ".kt" || e === ".kts" || e === ".scala"; |
| 678 | }); |
| 679 | const jvmSuffixMap = hasJvm ? buildJvmSuffixMap(fileSet) : undefined; |
| 680 | |
| 681 | // Build a namespace lookup map for C# projects. Each `namespace X.Y.Z` block |
| 682 | // (or file-scoped `namespace X.Y.Z;`) is recorded so `using X.Y.Z;` directives |
| 683 | // can be resolved to the file(s) that contribute to that namespace. Without |
| 684 | // this, every C# import resolved to null and the file graph was empty. |
| 685 | const hasCs = files.some((f) => path.extname(f).toLowerCase() === ".cs"); |
| 686 | const csNamespaceMap = hasCs ? buildCsNamespaceMap(fileSet, resolvedPath) : undefined; |
| 687 | |
| 688 | // Build Go module-resolution info from go.mod (issue #45). Without this, |
| 689 | // every Go import resolved to null and Go projects produced an empty |
| 690 | // file graph. The info is null when go.mod is missing or unparseable; |
| 691 | // the resolver treats null as "no Go resolution available" and behaves |
| 692 | // exactly as it did before this PR for those cases. |
| 693 | const hasGo = files.some((f) => f.endsWith(".go")); |
| 694 | const goModuleInfo = hasGo ? buildGoModuleInfo(fileSet, resolvedPath) : undefined; |
| 695 | |
| 696 | for (const relPath of files) { |
| 697 | const ext = path.extname(relPath).toLowerCase(); |
| 698 | const lang = getAstGrepLang(ext); |
| 699 | |
| 700 | // Files with no AST grammar (extra extensions) are included as leaf nodes |
| 701 | // so they can be targets of import edges from other files, but we skip |
no test coverage detected