gitnexus 1.4.8 → 1.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/cli/index-repo.d.ts +15 -0
- package/dist/cli/index-repo.js +115 -0
- package/dist/cli/index.js +11 -2
- package/dist/cli/setup.js +12 -9
- package/dist/cli/wiki.d.ts +4 -0
- package/dist/cli/wiki.js +174 -53
- package/dist/config/supported-languages.d.ts +7 -5
- package/dist/config/supported-languages.js +6 -4
- package/dist/core/graph/graph.js +9 -1
- package/dist/core/graph/types.d.ts +10 -2
- package/dist/core/ingestion/call-processor.d.ts +18 -1
- package/dist/core/ingestion/call-processor.js +297 -38
- package/dist/core/ingestion/call-routing.d.ts +3 -18
- package/dist/core/ingestion/call-routing.js +0 -19
- package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
- package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
- package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
- package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
- package/dist/core/ingestion/cobol-processor.d.ts +54 -0
- package/dist/core/ingestion/cobol-processor.js +1186 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
- package/dist/core/ingestion/entry-point-scoring.js +18 -4
- package/dist/core/ingestion/export-detection.d.ts +47 -8
- package/dist/core/ingestion/export-detection.js +29 -50
- package/dist/core/ingestion/field-extractor.d.ts +29 -0
- package/dist/core/ingestion/field-extractor.js +25 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
- package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
- package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
- package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
- package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
- package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
- package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
- package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
- package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
- package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
- package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
- package/dist/core/ingestion/field-extractors/generic.js +111 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
- package/dist/core/ingestion/field-extractors/typescript.js +291 -0
- package/dist/core/ingestion/field-types.d.ts +59 -0
- package/dist/core/ingestion/field-types.js +2 -0
- package/dist/core/ingestion/framework-detection.d.ts +87 -0
- package/dist/core/ingestion/framework-detection.js +65 -2
- package/dist/core/ingestion/heritage-processor.js +15 -17
- package/dist/core/ingestion/import-processor.d.ts +9 -10
- package/dist/core/ingestion/import-processor.js +59 -14
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
- package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/dart.js +44 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +9 -1
- package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.js +56 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/php.d.ts +6 -10
- package/dist/core/ingestion/{resolvers → import-resolvers}/php.js +7 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
- package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/swift.js +23 -0
- package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
- package/dist/core/ingestion/import-resolvers/types.js +6 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +0 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +0 -9
- package/dist/core/ingestion/language-config.d.ts +4 -1
- package/dist/core/ingestion/language-provider.d.ts +121 -0
- package/dist/core/ingestion/language-provider.js +24 -0
- package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
- package/dist/core/ingestion/languages/c-cpp.js +71 -0
- package/dist/core/ingestion/languages/cobol.d.ts +1 -0
- package/dist/core/ingestion/languages/cobol.js +26 -0
- package/dist/core/ingestion/languages/csharp.d.ts +8 -0
- package/dist/core/ingestion/languages/csharp.js +49 -0
- package/dist/core/ingestion/languages/dart.d.ts +12 -0
- package/dist/core/ingestion/languages/dart.js +58 -0
- package/dist/core/ingestion/languages/go.d.ts +11 -0
- package/dist/core/ingestion/languages/go.js +28 -0
- package/dist/core/ingestion/languages/index.d.ts +38 -0
- package/dist/core/ingestion/languages/index.js +63 -0
- package/dist/core/ingestion/languages/java.d.ts +9 -0
- package/dist/core/ingestion/languages/java.js +29 -0
- package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
- package/dist/core/ingestion/languages/kotlin.js +53 -0
- package/dist/core/ingestion/languages/php.d.ts +8 -0
- package/dist/core/ingestion/languages/php.js +145 -0
- package/dist/core/ingestion/languages/python.d.ts +12 -0
- package/dist/core/ingestion/languages/python.js +39 -0
- package/dist/core/ingestion/languages/ruby.d.ts +9 -0
- package/dist/core/ingestion/languages/ruby.js +44 -0
- package/dist/core/ingestion/languages/rust.d.ts +12 -0
- package/dist/core/ingestion/languages/rust.js +44 -0
- package/dist/core/ingestion/languages/swift.d.ts +12 -0
- package/dist/core/ingestion/languages/swift.js +133 -0
- package/dist/core/ingestion/languages/typescript.d.ts +10 -0
- package/dist/core/ingestion/languages/typescript.js +60 -0
- package/dist/core/ingestion/mro-processor.js +14 -15
- package/dist/core/ingestion/{named-binding-extraction.d.ts → named-binding-processor.d.ts} +0 -9
- package/dist/core/ingestion/named-binding-processor.js +42 -0
- package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/csharp.js +37 -0
- package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/java.js +29 -0
- package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
- package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/php.js +61 -0
- package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/python.js +49 -0
- package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/rust.js +64 -0
- package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
- package/dist/core/ingestion/named-bindings/types.js +6 -0
- package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/typescript.js +58 -0
- package/dist/core/ingestion/parsing-processor.d.ts +5 -1
- package/dist/core/ingestion/parsing-processor.js +115 -16
- package/dist/core/ingestion/pipeline.js +925 -424
- package/dist/core/ingestion/resolution-context.js +1 -1
- package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
- package/dist/core/ingestion/route-extractors/expo.js +36 -0
- package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
- package/dist/core/ingestion/route-extractors/middleware.js +143 -0
- package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
- package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
- package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
- package/dist/core/ingestion/route-extractors/php.js +21 -0
- package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
- package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +8 -7
- package/dist/core/ingestion/tree-sitter-queries.js +231 -9
- package/dist/core/ingestion/type-env.d.ts +14 -17
- package/dist/core/ingestion/type-env.js +66 -14
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/csharp.js +1 -1
- package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
- package/dist/core/ingestion/type-extractors/dart.js +371 -0
- package/dist/core/ingestion/type-extractors/jvm.js +1 -1
- package/dist/core/ingestion/type-extractors/shared.d.ts +1 -13
- package/dist/core/ingestion/type-extractors/shared.js +9 -102
- package/dist/core/ingestion/type-extractors/swift.js +334 -4
- package/dist/core/ingestion/type-extractors/types.d.ts +3 -1
- package/dist/core/ingestion/{ast-helpers.d.ts → utils/ast-helpers.d.ts} +16 -13
- package/dist/core/ingestion/{ast-helpers.js → utils/ast-helpers.js} +111 -32
- package/dist/core/ingestion/{call-analysis.js → utils/call-analysis.js} +37 -0
- package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
- package/dist/core/ingestion/utils/event-loop.js +5 -0
- package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
- package/dist/core/ingestion/utils/language-detection.js +70 -0
- package/dist/core/ingestion/utils/verbose.d.ts +1 -0
- package/dist/core/ingestion/utils/verbose.js +7 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +43 -2
- package/dist/core/ingestion/workers/parse-worker.js +361 -150
- package/dist/core/lbug/csv-generator.js +34 -1
- package/dist/core/lbug/lbug-adapter.js +6 -0
- package/dist/core/lbug/schema.d.ts +5 -3
- package/dist/core/lbug/schema.js +39 -2
- package/dist/core/tree-sitter/parser-loader.js +7 -1
- package/dist/core/wiki/cursor-client.d.ts +31 -0
- package/dist/core/wiki/cursor-client.js +127 -0
- package/dist/core/wiki/generator.d.ts +28 -9
- package/dist/core/wiki/generator.js +115 -18
- package/dist/core/wiki/graph-queries.d.ts +4 -0
- package/dist/core/wiki/graph-queries.js +7 -1
- package/dist/core/wiki/llm-client.d.ts +2 -0
- package/dist/core/wiki/llm-client.js +8 -4
- package/dist/core/wiki/prompts.d.ts +3 -3
- package/dist/core/wiki/prompts.js +6 -0
- package/dist/mcp/core/lbug-adapter.d.ts +5 -0
- package/dist/mcp/core/lbug-adapter.js +11 -1
- package/dist/mcp/local/local-backend.d.ts +16 -5
- package/dist/mcp/local/local-backend.js +711 -74
- package/dist/mcp/tools.js +71 -2
- package/dist/storage/repo-manager.d.ts +3 -0
- package/package.json +14 -14
- package/dist/core/ingestion/import-resolution.d.ts +0 -101
- package/dist/core/ingestion/import-resolution.js +0 -251
- package/dist/core/ingestion/named-binding-extraction.js +0 -373
- package/dist/core/ingestion/resolvers/index.d.ts +0 -18
- package/dist/core/ingestion/resolvers/index.js +0 -13
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
- package/dist/core/ingestion/type-extractors/index.js +0 -31
- package/dist/core/ingestion/utils.d.ts +0 -20
- package/dist/core/ingestion/utils.js +0 -242
- package/scripts/patch-tree-sitter-swift.cjs +0 -74
- /package/dist/core/ingestion/{call-analysis.d.ts → utils/call-analysis.d.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type NodeLabel = 'Project' | 'Package' | 'Module' | 'Folder' | 'File' | 'Class' | 'Function' | 'Method' | 'Variable' | 'Interface' | 'Enum' | 'Decorator' | 'Import' | 'Type' | 'CodeElement' | 'Community' | 'Process' | 'Struct' | 'Macro' | 'Typedef' | 'Union' | 'Namespace' | 'Trait' | 'Impl' | 'TypeAlias' | 'Const' | 'Static' | 'Property' | 'Record' | 'Delegate' | 'Annotation' | 'Constructor' | 'Template' | 'Section';
|
|
1
|
+
export type NodeLabel = 'Project' | 'Package' | 'Module' | 'Folder' | 'File' | 'Class' | 'Function' | 'Method' | 'Variable' | 'Interface' | 'Enum' | 'Decorator' | 'Import' | 'Type' | 'CodeElement' | 'Community' | 'Process' | 'Struct' | 'Macro' | 'Typedef' | 'Union' | 'Namespace' | 'Trait' | 'Impl' | 'TypeAlias' | 'Const' | 'Static' | 'Property' | 'Record' | 'Delegate' | 'Annotation' | 'Constructor' | 'Template' | 'Section' | 'Route' | 'Tool';
|
|
2
2
|
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
3
3
|
export type NodeProperties = {
|
|
4
4
|
name: string;
|
|
@@ -25,8 +25,15 @@ export type NodeProperties = {
|
|
|
25
25
|
parameterCount?: number;
|
|
26
26
|
level?: number;
|
|
27
27
|
returnType?: string;
|
|
28
|
+
declaredType?: string;
|
|
29
|
+
visibility?: string;
|
|
30
|
+
isStatic?: boolean;
|
|
31
|
+
isReadonly?: boolean;
|
|
32
|
+
responseKeys?: string[];
|
|
33
|
+
errorKeys?: string[];
|
|
34
|
+
middleware?: string[];
|
|
28
35
|
};
|
|
29
|
-
export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'OVERRIDES' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'MEMBER_OF' | 'STEP_IN_PROCESS';
|
|
36
|
+
export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'OVERRIDES' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'MEMBER_OF' | 'STEP_IN_PROCESS' | 'HANDLES_ROUTE' | 'FETCHES' | 'HANDLES_TOOL' | 'ENTRY_POINT_OF' | 'WRAPS' | 'QUERIES';
|
|
30
37
|
export interface GraphNode {
|
|
31
38
|
id: string;
|
|
32
39
|
label: NodeLabel;
|
|
@@ -64,4 +71,5 @@ export interface KnowledgeGraph {
|
|
|
64
71
|
addRelationship: (relationship: GraphRelationship) => void;
|
|
65
72
|
removeNode: (nodeId: string) => boolean;
|
|
66
73
|
removeNodesByFile: (filePath: string) => number;
|
|
74
|
+
removeRelationship: (relationshipId: string) => boolean;
|
|
67
75
|
}
|
|
@@ -2,7 +2,7 @@ import { KnowledgeGraph } from '../graph/types.js';
|
|
|
2
2
|
import { ASTCache } from './ast-cache.js';
|
|
3
3
|
import type { SymbolTable } from './symbol-table.js';
|
|
4
4
|
import type { ResolutionContext } from './resolution-context.js';
|
|
5
|
-
import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
|
|
5
|
+
import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, FileConstructorBindings } from './workers/parse-worker.js';
|
|
6
6
|
/** Per-file resolved type bindings for exported symbols.
|
|
7
7
|
* Populated during call processing, consumed by Phase 14 re-resolution pass. */
|
|
8
8
|
export type ExportedTypeMap = Map<string, Map<string, string>>;
|
|
@@ -68,3 +68,20 @@ export declare const processAssignmentsFromExtracted: (graph: KnowledgeGraph, as
|
|
|
68
68
|
* Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
|
|
69
69
|
*/
|
|
70
70
|
export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], ctx: ResolutionContext, onProgress?: (current: number, total: number) => void) => Promise<void>;
|
|
71
|
+
export declare const extractConsumerAccessedKeys: (content: string) => string[];
|
|
72
|
+
/**
|
|
73
|
+
* Create FETCHES edges from extracted fetch() calls to matching Route nodes.
|
|
74
|
+
* When consumerContents is provided, extracts property access patterns from
|
|
75
|
+
* consumer files and encodes them in the edge reason field.
|
|
76
|
+
*/
|
|
77
|
+
export declare const processNextjsFetchRoutes: (graph: KnowledgeGraph, fetchCalls: ExtractedFetchCall[], routeRegistry: Map<string, string>, // routeURL → handlerFilePath
|
|
78
|
+
consumerContents?: Map<string, string>) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Extract fetch() calls from source files (sequential path).
|
|
81
|
+
* Workers handle this via tree-sitter captures in parse-worker; this function
|
|
82
|
+
* provides the same extraction for the sequential fallback path.
|
|
83
|
+
*/
|
|
84
|
+
export declare const extractFetchCallsFromFiles: (files: {
|
|
85
|
+
path: string;
|
|
86
|
+
content: string;
|
|
87
|
+
}[], astCache: ASTCache) => Promise<ExtractedFetchCall[]>;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
2
|
import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
3
3
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getProvider } from './languages/index.js';
|
|
5
5
|
import { generateId } from '../../lib/utils.js';
|
|
6
|
-
import { getLanguageFromFilename
|
|
6
|
+
import { getLanguageFromFilename } from './utils/language-detection.js';
|
|
7
|
+
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
8
|
+
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
9
|
+
import { FUNCTION_NODE_TYPES, extractFunctionName, findEnclosingClassId } from './utils/ast-helpers.js';
|
|
10
|
+
import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from './utils/call-analysis.js';
|
|
7
11
|
import { buildTypeEnv, isSubclassOf } from './type-env.js';
|
|
8
12
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
9
|
-
import {
|
|
13
|
+
import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
10
14
|
import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
|
|
11
|
-
import { typeConfigs } from './type-extractors/index.js';
|
|
12
15
|
const MAX_EXPORTS_PER_FILE = 500;
|
|
13
16
|
const MAX_TYPE_NAME_LENGTH = 256;
|
|
14
17
|
/** Build a map of imported callee names → return types for cross-file call-result binding.
|
|
@@ -48,7 +51,7 @@ export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTabl
|
|
|
48
51
|
/** Collect resolved type bindings for exported file-scope symbols.
|
|
49
52
|
* Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. */
|
|
50
53
|
function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
|
|
51
|
-
const fileScope = typeEnv.
|
|
54
|
+
const fileScope = typeEnv.fileScope();
|
|
52
55
|
if (!fileScope || fileScope.size === 0)
|
|
53
56
|
return null;
|
|
54
57
|
const exported = new Map();
|
|
@@ -147,7 +150,7 @@ const TYPE_PRESERVING_METHODS = new Set([
|
|
|
147
150
|
* Walk up the AST from a node to find the enclosing function/method.
|
|
148
151
|
* Returns null if the call is at module/file level (top-level code).
|
|
149
152
|
*/
|
|
150
|
-
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
153
|
+
const findEnclosingFunction = (node, filePath, ctx, provider) => {
|
|
151
154
|
let current = node.parent;
|
|
152
155
|
while (current) {
|
|
153
156
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
@@ -157,7 +160,33 @@ const findEnclosingFunction = (node, filePath, ctx) => {
|
|
|
157
160
|
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
158
161
|
return resolved.candidates[0].nodeId;
|
|
159
162
|
}
|
|
160
|
-
|
|
163
|
+
// Apply labelOverride so label matches the definition phase (single source of truth).
|
|
164
|
+
let finalLabel = label;
|
|
165
|
+
if (provider.labelOverride) {
|
|
166
|
+
const override = provider.labelOverride(current, label);
|
|
167
|
+
if (override !== null)
|
|
168
|
+
finalLabel = override;
|
|
169
|
+
}
|
|
170
|
+
return generateId(finalLabel, `${filePath}:${funcName}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Language-specific enclosing function resolution (e.g., Dart where
|
|
174
|
+
// function_body is a sibling of function_signature, not a child).
|
|
175
|
+
if (provider.enclosingFunctionFinder) {
|
|
176
|
+
const customResult = provider.enclosingFunctionFinder(current);
|
|
177
|
+
if (customResult) {
|
|
178
|
+
// Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
|
|
179
|
+
const resolved = ctx.resolve(customResult.funcName, filePath);
|
|
180
|
+
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
181
|
+
return resolved.candidates[0].nodeId;
|
|
182
|
+
}
|
|
183
|
+
let finalLabel = customResult.label;
|
|
184
|
+
if (provider.labelOverride) {
|
|
185
|
+
const override = provider.labelOverride(current.previousSibling, finalLabel);
|
|
186
|
+
if (override !== null)
|
|
187
|
+
finalLabel = override;
|
|
188
|
+
}
|
|
189
|
+
return generateId(finalLabel, `${filePath}:${customResult.funcName}`);
|
|
161
190
|
}
|
|
162
191
|
}
|
|
163
192
|
current = current.parent;
|
|
@@ -246,7 +275,8 @@ importedRawReturnTypesMap) => {
|
|
|
246
275
|
}
|
|
247
276
|
continue;
|
|
248
277
|
}
|
|
249
|
-
const
|
|
278
|
+
const provider = getProvider(language);
|
|
279
|
+
const queryStr = provider.treeSitterQueries;
|
|
250
280
|
if (!queryStr)
|
|
251
281
|
continue;
|
|
252
282
|
await loadLanguage(language, file.path);
|
|
@@ -271,7 +301,6 @@ importedRawReturnTypesMap) => {
|
|
|
271
301
|
console.warn(`Query error for ${file.path}:`, queryError);
|
|
272
302
|
continue;
|
|
273
303
|
}
|
|
274
|
-
const lang = getLanguageFromFilename(file.path);
|
|
275
304
|
// Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
|
|
276
305
|
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
|
277
306
|
const fileParentMap = new Map();
|
|
@@ -318,18 +347,19 @@ importedRawReturnTypesMap) => {
|
|
|
318
347
|
const importedBindings = importedBindingsMap?.get(file.path);
|
|
319
348
|
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
|
320
349
|
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
|
321
|
-
const typeEnv =
|
|
350
|
+
const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
|
|
322
351
|
if (typeEnv && exportedTypeMap) {
|
|
323
352
|
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
|
324
353
|
if (fileExports)
|
|
325
354
|
exportedTypeMap.set(file.path, fileExports);
|
|
326
355
|
}
|
|
327
|
-
const callRouter =
|
|
328
|
-
const verifiedReceivers = typeEnv
|
|
356
|
+
const callRouter = provider.callRouter;
|
|
357
|
+
const verifiedReceivers = typeEnv.constructorBindings.length > 0
|
|
329
358
|
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
|
330
359
|
: new Map();
|
|
331
360
|
const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
|
|
332
361
|
ctx.enableCache(file.path);
|
|
362
|
+
const widenCache = new Map();
|
|
333
363
|
matches.forEach(match => {
|
|
334
364
|
const captureMap = {};
|
|
335
365
|
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
@@ -345,7 +375,7 @@ importedRawReturnTypesMap) => {
|
|
|
345
375
|
}
|
|
346
376
|
// Fall back to verified constructor bindings (mirrors CALLS resolution tier 2)
|
|
347
377
|
if (!receiverTypeName && receiverText && receiverIndex.size > 0) {
|
|
348
|
-
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
|
|
378
|
+
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
|
|
349
379
|
const funcName = enclosing ? extractFuncNameFromSourceId(enclosing) : '';
|
|
350
380
|
receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverText);
|
|
351
381
|
}
|
|
@@ -357,7 +387,7 @@ importedRawReturnTypesMap) => {
|
|
|
357
387
|
}
|
|
358
388
|
}
|
|
359
389
|
if (receiverTypeName) {
|
|
360
|
-
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
|
|
390
|
+
const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
|
|
361
391
|
const srcId = enclosing || generateId('File', file.path);
|
|
362
392
|
// Defer resolution: Ruby attr_accessor properties are registered during
|
|
363
393
|
// this same loop, so cross-file lookups fail if the declaring file hasn't
|
|
@@ -375,7 +405,7 @@ importedRawReturnTypesMap) => {
|
|
|
375
405
|
if (!nameNode)
|
|
376
406
|
return;
|
|
377
407
|
const calledName = nameNode.text;
|
|
378
|
-
const routed = callRouter(calledName, captureMap['call']);
|
|
408
|
+
const routed = callRouter?.(calledName, captureMap['call']);
|
|
379
409
|
if (routed) {
|
|
380
410
|
switch (routed.kind) {
|
|
381
411
|
case 'skip':
|
|
@@ -429,7 +459,7 @@ importedRawReturnTypesMap) => {
|
|
|
429
459
|
break;
|
|
430
460
|
}
|
|
431
461
|
}
|
|
432
|
-
if (
|
|
462
|
+
if (provider.isBuiltInName(calledName))
|
|
433
463
|
return;
|
|
434
464
|
const callNode = captureMap['call'];
|
|
435
465
|
const callForm = inferCallForm(callNode, nameNode);
|
|
@@ -475,7 +505,7 @@ importedRawReturnTypesMap) => {
|
|
|
475
505
|
}
|
|
476
506
|
// Fall back to verified constructor bindings for return type inference
|
|
477
507
|
if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
|
|
478
|
-
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
508
|
+
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider);
|
|
479
509
|
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
480
510
|
receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
|
|
481
511
|
}
|
|
@@ -489,7 +519,7 @@ importedRawReturnTypesMap) => {
|
|
|
489
519
|
}
|
|
490
520
|
}
|
|
491
521
|
// Hoist sourceId so it's available for ACCESSES edge emission during chain walk.
|
|
492
|
-
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
522
|
+
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx, provider);
|
|
493
523
|
const sourceId = enclosingFuncId || generateId('File', file.path);
|
|
494
524
|
// Fall back to mixed chain resolution when the receiver is a complex expression
|
|
495
525
|
// (field chain, call chain, or interleaved — e.g. user.address.city.save() or
|
|
@@ -520,7 +550,7 @@ importedRawReturnTypesMap) => {
|
|
|
520
550
|
}
|
|
521
551
|
// Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
|
|
522
552
|
// Only used when multiple candidates survive arity filtering — ~1-3% of calls.
|
|
523
|
-
const langConfig =
|
|
553
|
+
const langConfig = provider.typeConfig;
|
|
524
554
|
const hints = langConfig?.inferLiteralType
|
|
525
555
|
? { callNode, inferLiteralType: langConfig.inferLiteralType }
|
|
526
556
|
: undefined;
|
|
@@ -530,7 +560,7 @@ importedRawReturnTypesMap) => {
|
|
|
530
560
|
callForm,
|
|
531
561
|
receiverTypeName,
|
|
532
562
|
receiverName,
|
|
533
|
-
}, file.path, ctx, hints);
|
|
563
|
+
}, file.path, ctx, hints, widenCache);
|
|
534
564
|
if (!resolved)
|
|
535
565
|
return;
|
|
536
566
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
@@ -694,21 +724,20 @@ const tryOverloadDisambiguation = (candidates, hints) => {
|
|
|
694
724
|
}
|
|
695
725
|
return null;
|
|
696
726
|
};
|
|
697
|
-
|
|
698
|
-
* Resolve a function call to its target node ID using priority strategy:
|
|
699
|
-
* A. Narrow candidates by scope tier via ctx.resolve()
|
|
700
|
-
* B. Filter to callable symbol kinds (constructor-aware when callForm is set)
|
|
701
|
-
* C. Apply arity filtering when parameter metadata is available
|
|
702
|
-
* D. Apply receiver-type filtering for member calls with typed receivers
|
|
703
|
-
* E. Apply overload disambiguation via argument literal types (when available)
|
|
704
|
-
*
|
|
705
|
-
* If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
|
|
706
|
-
*/
|
|
707
|
-
const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
|
|
727
|
+
const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) => {
|
|
708
728
|
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
709
729
|
if (!tiered)
|
|
710
730
|
return null;
|
|
711
731
|
let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
|
732
|
+
// Swift/Kotlin: constructor calls look like free function calls (no `new` keyword).
|
|
733
|
+
// If free-form filtering found no callable candidates but the symbol resolves to a
|
|
734
|
+
// Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
|
|
735
|
+
if (filteredCandidates.length === 0 && call.callForm === 'free') {
|
|
736
|
+
const hasTypeTarget = tiered.candidates.some(c => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
|
|
737
|
+
if (hasTypeTarget) {
|
|
738
|
+
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
739
|
+
}
|
|
740
|
+
}
|
|
712
741
|
// Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
|
|
713
742
|
// The attribute access gives callForm='member', but the callee may be a Class — a valid
|
|
714
743
|
// constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
|
|
@@ -716,17 +745,35 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
|
|
|
716
745
|
if (filteredCandidates.length === 0 && call.callForm === 'member') {
|
|
717
746
|
filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
|
|
718
747
|
}
|
|
719
|
-
// Module-alias disambiguation: Python `import auth; auth.User()` —
|
|
720
|
-
// auth.py
|
|
721
|
-
//
|
|
722
|
-
|
|
748
|
+
// Module-alias disambiguation: Python `import auth; auth.User()` — receiverName='auth'
|
|
749
|
+
// selects auth.py via moduleAliasMap. Runs for ALL member calls with a known module alias,
|
|
750
|
+
// not just ambiguous ones — same-file tier may shadow the correct cross-module target when
|
|
751
|
+
// the caller defines a function with the same name as the callee (Issue #417).
|
|
752
|
+
if (call.callForm === 'member' && call.receiverName) {
|
|
723
753
|
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
|
724
754
|
if (aliasMap) {
|
|
725
755
|
const moduleFile = aliasMap.get(call.receiverName);
|
|
726
756
|
if (moduleFile) {
|
|
727
757
|
const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
|
|
728
|
-
if (aliasFiltered.length > 0)
|
|
758
|
+
if (aliasFiltered.length > 0) {
|
|
729
759
|
filteredCandidates = aliasFiltered;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
// Same-file tier returned a local match, but the alias points elsewhere.
|
|
763
|
+
// Widen to global candidates and filter to the aliased module's file.
|
|
764
|
+
// Use per-file widenCache to avoid repeated lookupFuzzy for the same
|
|
765
|
+
// calledName+moduleFile from multiple call sites in the same file.
|
|
766
|
+
const cacheKey = `${call.calledName}\0${moduleFile}`;
|
|
767
|
+
let fuzzyDefs = widenCache?.get(cacheKey);
|
|
768
|
+
if (!fuzzyDefs) {
|
|
769
|
+
fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
|
|
770
|
+
widenCache?.set(cacheKey, fuzzyDefs);
|
|
771
|
+
}
|
|
772
|
+
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm)
|
|
773
|
+
.filter(c => c.filePath === moduleFile);
|
|
774
|
+
if (widened.length > 0)
|
|
775
|
+
filteredCandidates = widened;
|
|
776
|
+
}
|
|
730
777
|
}
|
|
731
778
|
}
|
|
732
779
|
}
|
|
@@ -780,8 +827,20 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
|
|
|
780
827
|
if (disambiguated)
|
|
781
828
|
return toResolveResult(disambiguated, tiered.tier);
|
|
782
829
|
}
|
|
783
|
-
if (filteredCandidates.length !== 1)
|
|
830
|
+
if (filteredCandidates.length !== 1) {
|
|
831
|
+
// Deduplicate: Swift extensions create multiple Class nodes with the same name.
|
|
832
|
+
// When all candidates share the same type and differ only by file (extension vs
|
|
833
|
+
// primary definition), they represent the same symbol. Prefer the primary
|
|
834
|
+
// definition (shortest file path: Product.swift over ProductExtension.swift).
|
|
835
|
+
if (filteredCandidates.length > 1) {
|
|
836
|
+
const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type);
|
|
837
|
+
if (allSameType && (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
|
|
838
|
+
const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
|
|
839
|
+
return toResolveResult(sorted[0], tiered.tier);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
784
842
|
return null;
|
|
843
|
+
}
|
|
785
844
|
return toResolveResult(filteredCandidates[0], tiered.tier);
|
|
786
845
|
};
|
|
787
846
|
// ── Scope key helpers ────────────────────────────────────────────────────
|
|
@@ -997,6 +1056,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
997
1056
|
await yieldToEventLoop();
|
|
998
1057
|
}
|
|
999
1058
|
ctx.enableCache(filePath);
|
|
1059
|
+
const widenCache = new Map();
|
|
1000
1060
|
const receiverMap = fileReceiverTypes.get(filePath);
|
|
1001
1061
|
for (const call of calls) {
|
|
1002
1062
|
let effectiveCall = call;
|
|
@@ -1038,7 +1098,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1038
1098
|
}
|
|
1039
1099
|
}
|
|
1040
1100
|
}
|
|
1041
|
-
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
1101
|
+
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache);
|
|
1042
1102
|
if (!resolved)
|
|
1043
1103
|
continue;
|
|
1044
1104
|
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
@@ -1152,3 +1212,202 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
|
|
|
1152
1212
|
}
|
|
1153
1213
|
onProgress?.(extractedRoutes.length, extractedRoutes.length);
|
|
1154
1214
|
};
|
|
1215
|
+
/**
|
|
1216
|
+
* Extract property access keys from a consumer file's source code near fetch calls.
|
|
1217
|
+
*
|
|
1218
|
+
* Looks for three patterns after a fetch/response variable assignment:
|
|
1219
|
+
* 1. Destructuring: `const { data, pagination } = await res.json()`
|
|
1220
|
+
* 2. Property access: `response.data`, `result.items`
|
|
1221
|
+
* 3. Optional chaining: `data?.key1?.key2`
|
|
1222
|
+
*
|
|
1223
|
+
* Returns deduplicated top-level property names accessed on the response.
|
|
1224
|
+
*
|
|
1225
|
+
* NOTE: This scans the entire file content, not just code near a specific fetch call.
|
|
1226
|
+
* If a file has multiple fetch calls to different routes, all accessed keys are
|
|
1227
|
+
* attributed to each fetch. This is an acceptable tradeoff for regex-based extraction.
|
|
1228
|
+
*/
|
|
1229
|
+
/** Common method names on response/data objects that are NOT property accesses */
|
|
1230
|
+
// Properties/methods to ignore when extracting consumer accessed keys from `data.X` patterns.
|
|
1231
|
+
// Avoids false positives from Fetch API, Array, Object, Promise, and DOM access on variables
|
|
1232
|
+
// that happen to share names with response variables (data, result, response, etc.).
|
|
1233
|
+
const RESPONSE_ACCESS_BLOCKLIST = new Set([
|
|
1234
|
+
// Fetch/Response API
|
|
1235
|
+
'json', 'text', 'blob', 'arrayBuffer', 'formData', 'ok', 'status', 'headers', 'clone',
|
|
1236
|
+
// Promise
|
|
1237
|
+
'then', 'catch', 'finally',
|
|
1238
|
+
// Array
|
|
1239
|
+
'map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every',
|
|
1240
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat', 'join',
|
|
1241
|
+
'sort', 'reverse', 'includes', 'indexOf',
|
|
1242
|
+
// Object
|
|
1243
|
+
'length', 'toString', 'valueOf', 'keys', 'values', 'entries',
|
|
1244
|
+
// DOM methods — file-download patterns often reuse `data`/`response` variable names
|
|
1245
|
+
'appendChild', 'removeChild', 'insertBefore', 'replaceChild', 'replaceChildren',
|
|
1246
|
+
'createElement', 'getElementById', 'querySelector', 'querySelectorAll',
|
|
1247
|
+
'setAttribute', 'getAttribute', 'removeAttribute', 'hasAttribute',
|
|
1248
|
+
'addEventListener', 'removeEventListener', 'dispatchEvent',
|
|
1249
|
+
'classList', 'className',
|
|
1250
|
+
'parentNode', 'parentElement', 'childNodes', 'children',
|
|
1251
|
+
'nextSibling', 'previousSibling', 'firstChild', 'lastChild',
|
|
1252
|
+
'click', 'focus', 'blur', 'submit', 'reset',
|
|
1253
|
+
'innerHTML', 'outerHTML', 'textContent', 'innerText',
|
|
1254
|
+
]);
|
|
1255
|
+
export const extractConsumerAccessedKeys = (content) => {
|
|
1256
|
+
const keys = new Set();
|
|
1257
|
+
// Pattern 1: Destructuring from .json() — const { key1, key2 } = await res.json()
|
|
1258
|
+
// Also matches: const { key1, key2 } = await (await fetch(...)).json()
|
|
1259
|
+
const destructurePattern = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:await\s+)?(?:\w+\.json\s*\(\)|(?:await\s+)?(?:fetch|axios|got)\s*\([^)]*\)(?:\.then\s*\([^)]*\))?(?:\.json\s*\(\))?)/g;
|
|
1260
|
+
let match;
|
|
1261
|
+
while ((match = destructurePattern.exec(content)) !== null) {
|
|
1262
|
+
const destructuredBody = match[1];
|
|
1263
|
+
// Extract identifiers from destructuring, handling renamed bindings (key: alias)
|
|
1264
|
+
const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
|
|
1265
|
+
let keyMatch;
|
|
1266
|
+
while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
|
|
1267
|
+
keys.add(keyMatch[1]);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// Pattern 2: Destructuring from a data/result/response/json variable
|
|
1271
|
+
// e.g., const { items, total } = data; or const { error } = result;
|
|
1272
|
+
const dataVarDestructure = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:data|result|response|json|body|res)\b/g;
|
|
1273
|
+
while ((match = dataVarDestructure.exec(content)) !== null) {
|
|
1274
|
+
const destructuredBody = match[1];
|
|
1275
|
+
const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
|
|
1276
|
+
let keyMatch;
|
|
1277
|
+
while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
|
|
1278
|
+
keys.add(keyMatch[1]);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
// Pattern 3: Property access on common response variable names
|
|
1282
|
+
// Matches: data.key, response.key, result.key, json.key, body.key
|
|
1283
|
+
// Also matches optional chaining: data?.key
|
|
1284
|
+
const propAccessPattern = /\b(?:data|response|result|json|body|res)\s*(?:\?\.|\.)(\w+)/g;
|
|
1285
|
+
while ((match = propAccessPattern.exec(content)) !== null) {
|
|
1286
|
+
const key = match[1];
|
|
1287
|
+
// Skip common method calls that aren't property accesses
|
|
1288
|
+
if (!RESPONSE_ACCESS_BLOCKLIST.has(key)) {
|
|
1289
|
+
keys.add(key);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return [...keys];
|
|
1293
|
+
};
|
|
1294
|
+
/**
|
|
1295
|
+
* Create FETCHES edges from extracted fetch() calls to matching Route nodes.
|
|
1296
|
+
* When consumerContents is provided, extracts property access patterns from
|
|
1297
|
+
* consumer files and encodes them in the edge reason field.
|
|
1298
|
+
*/
|
|
1299
|
+
export const processNextjsFetchRoutes = (graph, fetchCalls, routeRegistry, // routeURL → handlerFilePath
|
|
1300
|
+
consumerContents) => {
|
|
1301
|
+
// Pre-count how many routes each consumer file matches (for confidence attribution)
|
|
1302
|
+
const routeCountByFile = new Map();
|
|
1303
|
+
for (const call of fetchCalls) {
|
|
1304
|
+
const normalized = normalizeFetchURL(call.fetchURL);
|
|
1305
|
+
if (!normalized)
|
|
1306
|
+
continue;
|
|
1307
|
+
for (const [routeURL] of routeRegistry) {
|
|
1308
|
+
if (routeMatches(normalized, routeURL)) {
|
|
1309
|
+
routeCountByFile.set(call.filePath, (routeCountByFile.get(call.filePath) ?? 0) + 1);
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
for (const call of fetchCalls) {
|
|
1315
|
+
const normalized = normalizeFetchURL(call.fetchURL);
|
|
1316
|
+
if (!normalized)
|
|
1317
|
+
continue;
|
|
1318
|
+
for (const [routeURL] of routeRegistry) {
|
|
1319
|
+
if (routeMatches(normalized, routeURL)) {
|
|
1320
|
+
const sourceId = generateId('File', call.filePath);
|
|
1321
|
+
const routeNodeId = generateId('Route', routeURL);
|
|
1322
|
+
// Extract consumer accessed keys if file content is available
|
|
1323
|
+
let reason = 'fetch-url-match';
|
|
1324
|
+
if (consumerContents) {
|
|
1325
|
+
const content = consumerContents.get(call.filePath);
|
|
1326
|
+
if (content) {
|
|
1327
|
+
const accessedKeys = extractConsumerAccessedKeys(content);
|
|
1328
|
+
if (accessedKeys.length > 0) {
|
|
1329
|
+
reason = `fetch-url-match|keys:${accessedKeys.join(',')}`;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
// Encode multi-fetch count so downstream can set confidence
|
|
1334
|
+
const fetchCount = routeCountByFile.get(call.filePath) ?? 1;
|
|
1335
|
+
if (fetchCount > 1) {
|
|
1336
|
+
reason = `${reason}|fetches:${fetchCount}`;
|
|
1337
|
+
}
|
|
1338
|
+
graph.addRelationship({
|
|
1339
|
+
id: generateId('FETCHES', `${sourceId}->${routeNodeId}`),
|
|
1340
|
+
sourceId,
|
|
1341
|
+
targetId: routeNodeId,
|
|
1342
|
+
type: 'FETCHES',
|
|
1343
|
+
confidence: 0.9,
|
|
1344
|
+
reason,
|
|
1345
|
+
});
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1352
|
+
* Extract fetch() calls from source files (sequential path).
|
|
1353
|
+
* Workers handle this via tree-sitter captures in parse-worker; this function
|
|
1354
|
+
* provides the same extraction for the sequential fallback path.
|
|
1355
|
+
*/
|
|
1356
|
+
export const extractFetchCallsFromFiles = async (files, astCache) => {
|
|
1357
|
+
const parser = await loadParser();
|
|
1358
|
+
const result = [];
|
|
1359
|
+
for (const file of files) {
|
|
1360
|
+
const language = getLanguageFromFilename(file.path);
|
|
1361
|
+
if (!language)
|
|
1362
|
+
continue;
|
|
1363
|
+
if (!isLanguageAvailable(language))
|
|
1364
|
+
continue;
|
|
1365
|
+
const provider = getProvider(language);
|
|
1366
|
+
const queryStr = provider.treeSitterQueries;
|
|
1367
|
+
if (!queryStr)
|
|
1368
|
+
continue;
|
|
1369
|
+
await loadLanguage(language, file.path);
|
|
1370
|
+
let tree = astCache.get(file.path);
|
|
1371
|
+
if (!tree) {
|
|
1372
|
+
try {
|
|
1373
|
+
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
astCache.set(file.path, tree);
|
|
1379
|
+
}
|
|
1380
|
+
let matches;
|
|
1381
|
+
try {
|
|
1382
|
+
const lang = parser.getLanguage();
|
|
1383
|
+
const query = new Parser.Query(lang, queryStr);
|
|
1384
|
+
matches = query.matches(tree.rootNode);
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
for (const match of matches) {
|
|
1390
|
+
const captureMap = {};
|
|
1391
|
+
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
1392
|
+
if (captureMap['route.fetch']) {
|
|
1393
|
+
const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
|
|
1394
|
+
if (urlNode) {
|
|
1395
|
+
result.push({
|
|
1396
|
+
filePath: file.path,
|
|
1397
|
+
fetchURL: urlNode.text,
|
|
1398
|
+
lineNumber: captureMap['route.fetch'].startPosition.row,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
else if (captureMap['http_client'] && captureMap['http_client.url']) {
|
|
1403
|
+
const method = captureMap['http_client.method']?.text;
|
|
1404
|
+
const url = captureMap['http_client.url'].text;
|
|
1405
|
+
const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
|
|
1406
|
+
if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
|
|
1407
|
+
result.push({ filePath: file.path, fetchURL: url, lineNumber: captureMap['http_client'].startPosition.row });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1413
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* two packages have separate build targets (Node native vs WASM/browser).
|
|
11
11
|
* Keep both copies in sync until a shared package is introduced.
|
|
12
12
|
*/
|
|
13
|
+
import type { SyntaxNode } from './utils/ast-helpers.js';
|
|
13
14
|
/** null = this call was not routed; fall through to default call handling */
|
|
14
15
|
export type CallRoutingResult = RubyCallRouting | null;
|
|
15
16
|
/**
|
|
@@ -18,23 +19,7 @@ export type CallRoutingResult = RubyCallRouting | null;
|
|
|
18
19
|
* returns an importPath MUST validate it independently (length cap, control-char
|
|
19
20
|
* rejection). See routeRubyCall for the reference implementation.
|
|
20
21
|
*/
|
|
21
|
-
export type CallRouter = (calledName: string, callNode:
|
|
22
|
-
/** Per-language call routing. noRouting = no special routing (normal call processing) */
|
|
23
|
-
export declare const callRouters: {
|
|
24
|
-
javascript: CallRouter;
|
|
25
|
-
typescript: CallRouter;
|
|
26
|
-
python: CallRouter;
|
|
27
|
-
java: CallRouter;
|
|
28
|
-
kotlin: CallRouter;
|
|
29
|
-
go: CallRouter;
|
|
30
|
-
rust: CallRouter;
|
|
31
|
-
csharp: CallRouter;
|
|
32
|
-
php: CallRouter;
|
|
33
|
-
swift: CallRouter;
|
|
34
|
-
cpp: CallRouter;
|
|
35
|
-
c: CallRouter;
|
|
36
|
-
ruby: typeof routeRubyCall;
|
|
37
|
-
};
|
|
22
|
+
export type CallRouter = (calledName: string, callNode: SyntaxNode) => CallRoutingResult;
|
|
38
23
|
export type RubyCallRouting = {
|
|
39
24
|
kind: 'import';
|
|
40
25
|
importPath: string;
|
|
@@ -71,4 +56,4 @@ export interface RubyPropertyItem {
|
|
|
71
56
|
* @param callNode - The tree-sitter `call` AST node
|
|
72
57
|
* @returns A discriminated union describing the call's semantic role
|
|
73
58
|
*/
|
|
74
|
-
export declare function routeRubyCall(calledName: string, callNode:
|
|
59
|
+
export declare function routeRubyCall(calledName: string, callNode: SyntaxNode): RubyCallRouting;
|
|
@@ -10,25 +10,6 @@
|
|
|
10
10
|
* two packages have separate build targets (Node native vs WASM/browser).
|
|
11
11
|
* Keep both copies in sync until a shared package is introduced.
|
|
12
12
|
*/
|
|
13
|
-
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
14
|
-
/** No-op router: returns null for every call (passthrough to normal processing) */
|
|
15
|
-
const noRouting = () => null;
|
|
16
|
-
/** Per-language call routing. noRouting = no special routing (normal call processing) */
|
|
17
|
-
export const callRouters = {
|
|
18
|
-
[SupportedLanguages.JavaScript]: noRouting,
|
|
19
|
-
[SupportedLanguages.TypeScript]: noRouting,
|
|
20
|
-
[SupportedLanguages.Python]: noRouting,
|
|
21
|
-
[SupportedLanguages.Java]: noRouting,
|
|
22
|
-
[SupportedLanguages.Kotlin]: noRouting,
|
|
23
|
-
[SupportedLanguages.Go]: noRouting,
|
|
24
|
-
[SupportedLanguages.Rust]: noRouting,
|
|
25
|
-
[SupportedLanguages.CSharp]: noRouting,
|
|
26
|
-
[SupportedLanguages.PHP]: noRouting,
|
|
27
|
-
[SupportedLanguages.Swift]: noRouting,
|
|
28
|
-
[SupportedLanguages.CPlusPlus]: noRouting,
|
|
29
|
-
[SupportedLanguages.C]: noRouting,
|
|
30
|
-
[SupportedLanguages.Ruby]: routeRubyCall,
|
|
31
|
-
};
|
|
32
13
|
// ── Pre-allocated singletons for common return values ────────────────────────
|
|
33
14
|
const CALL_RESULT = { kind: 'call' };
|
|
34
15
|
const SKIP_RESULT = { kind: 'skip' };
|