gitnexus 1.6.0 → 1.6.1

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.
Files changed (136) hide show
  1. package/dist/cli/analyze.js +28 -3
  2. package/dist/core/group/extractors/fs-utils.d.ts +10 -0
  3. package/dist/core/group/extractors/fs-utils.js +24 -0
  4. package/dist/core/group/extractors/grpc-extractor.d.ts +17 -8
  5. package/dist/core/group/extractors/grpc-extractor.js +313 -191
  6. package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
  7. package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
  8. package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
  9. package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
  10. package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
  11. package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
  12. package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
  13. package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
  14. package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
  15. package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
  16. package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
  17. package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
  18. package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
  19. package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
  20. package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
  21. package/dist/core/group/extractors/http-patterns/go.js +215 -0
  22. package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
  23. package/dist/core/group/extractors/http-patterns/index.js +44 -0
  24. package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
  25. package/dist/core/group/extractors/http-patterns/java.js +253 -0
  26. package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
  27. package/dist/core/group/extractors/http-patterns/node.js +354 -0
  28. package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
  29. package/dist/core/group/extractors/http-patterns/php.js +70 -0
  30. package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
  31. package/dist/core/group/extractors/http-patterns/python.js +133 -0
  32. package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
  33. package/dist/core/group/extractors/http-patterns/types.js +1 -0
  34. package/dist/core/group/extractors/http-route-extractor.d.ts +10 -13
  35. package/dist/core/group/extractors/http-route-extractor.js +201 -238
  36. package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
  37. package/dist/core/group/extractors/manifest-extractor.js +235 -0
  38. package/dist/core/group/extractors/topic-extractor.d.ts +0 -1
  39. package/dist/core/group/extractors/topic-extractor.js +55 -192
  40. package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
  41. package/dist/core/group/extractors/topic-patterns/go.js +120 -0
  42. package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
  43. package/dist/core/group/extractors/topic-patterns/index.js +38 -0
  44. package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
  45. package/dist/core/group/extractors/topic-patterns/java.js +80 -0
  46. package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
  47. package/dist/core/group/extractors/topic-patterns/node.js +155 -0
  48. package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
  49. package/dist/core/group/extractors/topic-patterns/python.js +116 -0
  50. package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
  51. package/dist/core/group/extractors/topic-patterns/types.js +10 -0
  52. package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
  53. package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
  54. package/dist/core/ingestion/binding-accumulator.d.ts +22 -17
  55. package/dist/core/ingestion/binding-accumulator.js +29 -25
  56. package/dist/core/ingestion/cobol-processor.d.ts +1 -1
  57. package/dist/core/ingestion/import-processor.js +1 -1
  58. package/dist/core/ingestion/language-config.js +1 -1
  59. package/dist/core/ingestion/language-provider.d.ts +8 -0
  60. package/dist/core/ingestion/languages/ruby.js +15 -0
  61. package/dist/core/ingestion/markdown-processor.d.ts +1 -1
  62. package/dist/core/ingestion/method-extractors/configs/jvm.js +1 -0
  63. package/dist/core/ingestion/method-extractors/configs/ruby.js +1 -0
  64. package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
  65. package/dist/core/ingestion/method-extractors/generic.js +48 -4
  66. package/dist/core/ingestion/method-types.d.ts +4 -0
  67. package/dist/core/ingestion/model/resolve.js +103 -48
  68. package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
  69. package/dist/core/ingestion/model/semantic-model.js +1 -1
  70. package/dist/core/ingestion/model/symbol-table.d.ts +7 -7
  71. package/dist/core/ingestion/model/symbol-table.js +7 -7
  72. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  73. package/dist/core/ingestion/mro-processor.js +1 -1
  74. package/dist/core/ingestion/parsing-processor.js +54 -42
  75. package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
  76. package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
  77. package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
  78. package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
  79. package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
  80. package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
  81. package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
  82. package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
  83. package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
  84. package/dist/core/ingestion/pipeline-phases/index.js +22 -0
  85. package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
  86. package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
  87. package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
  88. package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
  89. package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
  90. package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
  91. package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
  92. package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
  93. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
  94. package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
  95. package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
  96. package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
  97. package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
  98. package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
  99. package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
  100. package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
  101. package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
  102. package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
  103. package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
  104. package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
  105. package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
  106. package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
  107. package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
  108. package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
  109. package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
  110. package/dist/core/ingestion/pipeline-phases/types.js +37 -0
  111. package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
  112. package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
  113. package/dist/core/ingestion/pipeline.d.ts +16 -10
  114. package/dist/core/ingestion/pipeline.js +66 -1534
  115. package/dist/core/ingestion/process-processor.js +1 -1
  116. package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
  117. package/dist/core/ingestion/tree-sitter-queries.js +69 -0
  118. package/dist/core/ingestion/utils/ast-helpers.d.ts +1 -3
  119. package/dist/core/ingestion/utils/ast-helpers.js +48 -21
  120. package/dist/core/ingestion/utils/env.d.ts +10 -0
  121. package/dist/core/ingestion/utils/env.js +10 -0
  122. package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
  123. package/dist/core/ingestion/utils/graph-sort.js +100 -0
  124. package/dist/core/ingestion/workers/parse-worker.js +12 -8
  125. package/dist/core/lbug/lbug-adapter.js +66 -24
  126. package/package.json +3 -3
  127. package/vendor/tree-sitter-proto/binding.gyp +30 -0
  128. package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
  129. package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
  130. package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
  131. package/vendor/tree-sitter-proto/package.json +18 -0
  132. package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
  133. package/vendor/tree-sitter-proto/src/parser.c +10149 -0
  134. package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
  135. package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
  136. package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
@@ -36,9 +36,12 @@
36
36
  * or (b) post-process worker-path entries through a follow-up resolution
37
37
  * pass after the main-thread `SymbolTable` is complete.
38
38
  *
39
- * **Lifecycle contract**: `append → finalize → consume → dispose`. See
40
- * `finalize()` and `dispose()` for the state machine. Disposal is
41
- * orthogonal to finalization: either order is legal.
39
+ * **Lifecycle contract**: single-use — `append* → finalize → consume → dispose`.
40
+ * After `dispose()` the accumulator is permanently dead: any mutating call
41
+ * (`appendFile`) throws, and read methods return empty/undefined as if the
42
+ * accumulator had never been appended to. The instance is not recyclable;
43
+ * construct a new one for a new pipeline run. Finalization and disposal are
44
+ * orthogonal state dimensions and may be invoked in either order.
42
45
  */
43
46
  export interface BindingEntry {
44
47
  readonly scope: string;
@@ -119,28 +122,30 @@ export declare class BindingAccumulator {
119
122
  finalize(): void;
120
123
  /**
121
124
  * Release the accumulator's heap footprint. Clears both internal storage
122
- * maps and resets `_totalBindings` to zero. Idempotent and orthogonal to
123
- * `finalize()` — calling `dispose()` does not change the finalized state.
125
+ * maps and resets `_totalBindings` to zero. Idempotent calling twice
126
+ * is a no-op. Orthogonal to `finalize()` — calling `dispose()` does not
127
+ * change the finalized state.
124
128
  *
125
- * Post-dispose contract: all read methods return empty/undefined state
126
- * matching a never-appended-to accumulator. Specifically:
129
+ * **Single-use lifecycle.** This is a one-way terminal transition: the
130
+ * accumulator is not recyclable. Any subsequent `appendFile` call throws
131
+ * (`'BindingAccumulator: use after dispose'`), regardless of whether
132
+ * `finalize()` was called first. Post-dispose reads do not throw —
133
+ * they return empty/undefined state matching a never-appended-to
134
+ * accumulator:
127
135
  * - `fileCount === 0`
128
136
  * - `totalBindings === 0`
129
137
  * - `files()` yields an empty iterator
130
138
  * - `getFile(x)` returns `undefined` for all `x`
131
139
  * - `fileScopeEntries(x)` returns `[]` for all `x`
140
+ * - `fileScopeGet(x, y)` returns `undefined` for all `x, y`
132
141
  * - `estimateMemoryBytes()` returns `0`
133
142
  *
134
- * If `dispose()` is called **before** `finalize()`, subsequent `appendFile`
135
- * calls succeed the accumulator behaves like a fresh one. If called
136
- * **after** `finalize()`, subsequent `appendFile` calls throw the existing
137
- * "finalized" error.
138
- *
139
- * Lifecycle note: the pipeline disposes the accumulator after both Phase 9
140
- * consumers (`processCallsFromExtracted`, `processAssignmentsFromExtracted`)
141
- * and the ExportedTypeMap enrichment loop have completed, so the heap is
142
- * released before Phase 14 (`runCrossFileBindingPropagation`) and
143
- * `runGraphAnalysisPhases` begin their long-running work.
143
+ * Lifecycle note: the pipeline disposes the accumulator inside the
144
+ * `finally` of the `crossFile` phase, which is scheduled after every
145
+ * other accumulator consumer (Phase 9 call/assignment processing and
146
+ * the ExportedTypeMap enrichment loop). The dispose call therefore
147
+ * runs once, on both the happy path and the throw path of the
148
+ * crossFile phase.
144
149
  */
145
150
  dispose(): void;
146
151
  /** Get all bindings for a file, or undefined if the file is unknown. */
@@ -36,9 +36,12 @@
36
36
  * or (b) post-process worker-path entries through a follow-up resolution
37
37
  * pass after the main-thread `SymbolTable` is complete.
38
38
  *
39
- * **Lifecycle contract**: `append → finalize → consume → dispose`. See
40
- * `finalize()` and `dispose()` for the state machine. Disposal is
41
- * orthogonal to finalization: either order is legal.
39
+ * **Lifecycle contract**: single-use — `append* → finalize → consume → dispose`.
40
+ * After `dispose()` the accumulator is permanently dead: any mutating call
41
+ * (`appendFile`) throws, and read methods return empty/undefined as if the
42
+ * accumulator had never been appended to. The instance is not recyclable;
43
+ * construct a new one for a new pipeline run. Finalization and disposal are
44
+ * orthogonal state dimensions and may be invoked in either order.
42
45
  */
43
46
  /**
44
47
  * Merge file-scope bindings from a (finalized) `BindingAccumulator` into an
@@ -137,17 +140,16 @@ export class BindingAccumulator {
137
140
  if (this._finalized) {
138
141
  throw new Error('[BindingAccumulator] appendFile after finalize — no further appends allowed');
139
142
  }
143
+ // Single-use lifecycle: once disposed, the accumulator is dead. A
144
+ // post-dispose append almost always indicates a missed wiring step
145
+ // (the consumer is reading state that was supposed to be released),
146
+ // so convert the silent use-after-dispose into a loud failure.
147
+ if (this._disposed) {
148
+ throw new Error('BindingAccumulator: use after dispose');
149
+ }
140
150
  if (entries.length === 0) {
141
151
  return;
142
152
  }
143
- // Contract consistency: if this accumulator was previously disposed
144
- // without being finalized, `dispose()` is documented to leave it
145
- // "behaving like a fresh one" for subsequent appends. Clear the
146
- // `_disposed` flag here so the `disposed` getter tracks the actual
147
- // live state, not a stale signal from the prior lifecycle cycle.
148
- if (this._disposed) {
149
- this._disposed = false;
150
- }
151
153
  // Note on the file-scope-only invariant:
152
154
  // The accumulator does NOT reject function-scope entries at this
153
155
  // boundary. The narrowing contract is enforced by the two production
@@ -213,28 +215,30 @@ export class BindingAccumulator {
213
215
  }
214
216
  /**
215
217
  * Release the accumulator's heap footprint. Clears both internal storage
216
- * maps and resets `_totalBindings` to zero. Idempotent and orthogonal to
217
- * `finalize()` — calling `dispose()` does not change the finalized state.
218
+ * maps and resets `_totalBindings` to zero. Idempotent calling twice
219
+ * is a no-op. Orthogonal to `finalize()` — calling `dispose()` does not
220
+ * change the finalized state.
218
221
  *
219
- * Post-dispose contract: all read methods return empty/undefined state
220
- * matching a never-appended-to accumulator. Specifically:
222
+ * **Single-use lifecycle.** This is a one-way terminal transition: the
223
+ * accumulator is not recyclable. Any subsequent `appendFile` call throws
224
+ * (`'BindingAccumulator: use after dispose'`), regardless of whether
225
+ * `finalize()` was called first. Post-dispose reads do not throw —
226
+ * they return empty/undefined state matching a never-appended-to
227
+ * accumulator:
221
228
  * - `fileCount === 0`
222
229
  * - `totalBindings === 0`
223
230
  * - `files()` yields an empty iterator
224
231
  * - `getFile(x)` returns `undefined` for all `x`
225
232
  * - `fileScopeEntries(x)` returns `[]` for all `x`
233
+ * - `fileScopeGet(x, y)` returns `undefined` for all `x, y`
226
234
  * - `estimateMemoryBytes()` returns `0`
227
235
  *
228
- * If `dispose()` is called **before** `finalize()`, subsequent `appendFile`
229
- * calls succeed the accumulator behaves like a fresh one. If called
230
- * **after** `finalize()`, subsequent `appendFile` calls throw the existing
231
- * "finalized" error.
232
- *
233
- * Lifecycle note: the pipeline disposes the accumulator after both Phase 9
234
- * consumers (`processCallsFromExtracted`, `processAssignmentsFromExtracted`)
235
- * and the ExportedTypeMap enrichment loop have completed, so the heap is
236
- * released before Phase 14 (`runCrossFileBindingPropagation`) and
237
- * `runGraphAnalysisPhases` begin their long-running work.
236
+ * Lifecycle note: the pipeline disposes the accumulator inside the
237
+ * `finally` of the `crossFile` phase, which is scheduled after every
238
+ * other accumulator consumer (Phase 9 call/assignment processing and
239
+ * the ExportedTypeMap enrichment loop). The dispose call therefore
240
+ * runs once, on both the happy path and the throw path of the
241
+ * crossFile phase.
238
242
  */
239
243
  dispose() {
240
244
  this._allByFile.clear();
@@ -50,5 +50,5 @@ export declare function isJclFile(filePath: string): boolean;
50
50
  * @param allPathSet - Set of all file paths in the repository
51
51
  * @returns Summary of what was extracted
52
52
  */
53
- export declare const processCobol: (graph: KnowledgeGraph, files: CobolFile[], allPathSet: Set<string>) => CobolProcessResult;
53
+ export declare const processCobol: (graph: KnowledgeGraph, files: CobolFile[], allPathSet: ReadonlySet<string>) => CobolProcessResult;
54
54
  export {};
@@ -8,7 +8,7 @@ import { yieldToEventLoop } from './utils/event-loop.js';
8
8
  import { getTreeSitterBufferSize } from './constants.js';
9
9
  import { loadImportConfigs } from './language-config.js';
10
10
  import { buildSuffixIndex } from './import-resolvers/utils.js';
11
- const isDev = process.env.NODE_ENV === 'development';
11
+ import { isDev } from './utils/env.js';
12
12
  /** Group files by provider (only those with implicit import wiring), then call each wirer
13
13
  * with its own language's files. O(n) over files, O(1) per provider lookup. */
14
14
  function wireImplicitImports(files, importMap, addImportEdge, projectConfig) {
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- const isDev = process.env.NODE_ENV === 'development';
3
+ import { isDev } from './utils/env.js';
4
4
  // ============================================================================
5
5
  // LANGUAGE-SPECIFIC CONFIG LOADERS
6
6
  // ============================================================================
@@ -66,6 +66,14 @@ interface LanguageProviderConfig {
66
66
  * Called with only THIS language's files (pre-grouped by the processor).
67
67
  * Default: undefined (no implicit imports). */
68
68
  readonly implicitImportWirer?: (languageFiles: string[], importMap: ReadonlyMap<string, ReadonlySet<string>>, addImportEdge: (src: string, target: string) => void, projectConfig: unknown) => void;
69
+ /** Resolve a container node during enclosing-owner tree walks.
70
+ * Called when a CLASS_CONTAINER_TYPES node is found while walking up.
71
+ * - Return a different SyntaxNode to remap the container (e.g., Ruby
72
+ * singleton_class → enclosing class/module).
73
+ * - Return null to skip this container and keep walking up.
74
+ * - Omit (undefined) to use the container node as-is (default).
75
+ * Default: undefined (no remapping). */
76
+ readonly resolveEnclosingOwner?: (node: SyntaxNode) => SyntaxNode | null;
69
77
  /** Resolve the enclosing function name + label from an AST ancestor node
70
78
  * that is NOT a standard FUNCTION_NODE_TYPE. For languages where the
71
79
  * function body is a sibling of the signature (e.g. Dart: function_body ↔
@@ -100,6 +100,21 @@ export const rubyProvider = defineLanguage({
100
100
  importResolver: resolveRubyImport,
101
101
  callRouter: routeRubyCall,
102
102
  importSemantics: 'wildcard',
103
+ resolveEnclosingOwner(node) {
104
+ // Ruby singleton_class (class << self) should resolve to the enclosing
105
+ // class or module for owner/container resolution (HAS_METHOD edges, class IDs).
106
+ if (node.type === 'singleton_class') {
107
+ let ancestor = node.parent;
108
+ while (ancestor) {
109
+ if (ancestor.type === 'class' || ancestor.type === 'module') {
110
+ return ancestor;
111
+ }
112
+ ancestor = ancestor.parent;
113
+ }
114
+ return null; // no enclosing class/module — skip
115
+ }
116
+ return node; // use as-is for all other container types
117
+ },
103
118
  fieldExtractor: createFieldExtractor(rubyFieldConfig),
104
119
  methodExtractor: createMethodExtractor({
105
120
  ...rubyMethodConfig,
@@ -10,7 +10,7 @@ interface MdFile {
10
10
  path: string;
11
11
  content: string;
12
12
  }
13
- export declare const processMarkdown: (graph: KnowledgeGraph, files: MdFile[], allPathSet: Set<string>) => {
13
+ export declare const processMarkdown: (graph: KnowledgeGraph, files: MdFile[], allPathSet: ReadonlySet<string>) => {
14
14
  sections: number;
15
15
  links: number;
16
16
  };
@@ -248,6 +248,7 @@ export const kotlinMethodConfig = {
248
248
  typeDeclarationNodes: ['class_declaration', 'object_declaration', 'companion_object'],
249
249
  methodNodeTypes: ['function_declaration'],
250
250
  bodyNodeTypes: ['class_body'],
251
+ staticOwnerTypes: new Set(['companion_object', 'object_declaration']),
251
252
  extractName(node) {
252
253
  for (let i = 0; i < node.namedChildCount; i++) {
253
254
  const child = node.namedChild(i);
@@ -202,6 +202,7 @@ export const rubyMethodConfig = {
202
202
  typeDeclarationNodes: ['class', 'module', 'singleton_class'],
203
203
  methodNodeTypes: ['method', 'singleton_method'],
204
204
  bodyNodeTypes: ['body_statement'],
205
+ staticOwnerTypes: new Set(['singleton_class']),
205
206
  extractOwnerName(node) {
206
207
  // singleton_class (class << self) inherits the enclosing class/module name
207
208
  if (node.type === 'singleton_class') {
@@ -1,5 +1,11 @@
1
1
  import type { MethodExtractor, MethodExtractionConfig } from '../method-types.js';
2
2
  /**
3
3
  * Create a MethodExtractor from a declarative config.
4
+ *
5
+ * @throws {Error} if `typeDeclarationNodes` contains a static-implying owner
6
+ * type (companion_object / object_declaration / singleton_class) that is
7
+ * not covered by `staticOwnerTypes`. The guard fires once per language at
8
+ * provider construction to prevent silent `isStatic=false` regressions. See
9
+ * `STATIC_IMPLYING_OWNER_TYPES` for the exact opt-out convention.
4
10
  */
5
11
  export declare function createMethodExtractor(config: MethodExtractionConfig): MethodExtractor;
@@ -1,10 +1,53 @@
1
1
  // gitnexus/src/core/ingestion/method-extractors/generic.ts
2
- /** Owner node types where member functions are effectively static (JVM/Ruby semantics). */
3
- const STATIC_OWNER_TYPES = new Set(['companion_object', 'object_declaration', 'singleton_class']);
2
+ /**
3
+ * Node types that imply static member semantics when they appear as the owner
4
+ * of a method (Kotlin companion objects, Kotlin top-level `object` declarations,
5
+ * Ruby `class << self` singleton classes). A config that lists any of these in
6
+ * `typeDeclarationNodes` MUST also include the same node type in
7
+ * `staticOwnerTypes` — otherwise methods inside these containers silently get
8
+ * `isStatic=false`, which is a correctness bug that previously only surfaced
9
+ * at analysis time on large repos.
10
+ *
11
+ * Opt-out: a config that sets `staticOwnerTypes: new Set()` (explicit empty
12
+ * set) signals "I handle static-ness entirely via isStatic()" and is exempt
13
+ * from the guard.
14
+ */
15
+ const STATIC_IMPLYING_OWNER_TYPES = new Set([
16
+ 'companion_object',
17
+ 'object_declaration',
18
+ 'singleton_class',
19
+ ]);
4
20
  /**
5
21
  * Create a MethodExtractor from a declarative config.
22
+ *
23
+ * @throws {Error} if `typeDeclarationNodes` contains a static-implying owner
24
+ * type (companion_object / object_declaration / singleton_class) that is
25
+ * not covered by `staticOwnerTypes`. The guard fires once per language at
26
+ * provider construction to prevent silent `isStatic=false` regressions. See
27
+ * `STATIC_IMPLYING_OWNER_TYPES` for the exact opt-out convention.
6
28
  */
7
29
  export function createMethodExtractor(config) {
30
+ // Runtime invariant: each static-implying container type declared in
31
+ // typeDeclarationNodes must be covered by staticOwnerTypes. An explicit
32
+ // empty Set is treated as intentional opt-out.
33
+ if (config.staticOwnerTypes === undefined) {
34
+ const missing = config.typeDeclarationNodes.filter((t) => STATIC_IMPLYING_OWNER_TYPES.has(t));
35
+ if (missing.length > 0) {
36
+ throw new Error(`[MethodExtractionConfig:${config.language}] typeDeclarationNodes includes static-implying owner type(s) ` +
37
+ `${JSON.stringify(missing)} but staticOwnerTypes is not set. Add ` +
38
+ `'staticOwnerTypes: new Set([${missing.map((t) => `'${t}'`).join(', ')}])' ` +
39
+ `to the config, or set 'staticOwnerTypes: new Set()' to opt out explicitly.`);
40
+ }
41
+ }
42
+ else {
43
+ const missing = config.typeDeclarationNodes.filter((t) => STATIC_IMPLYING_OWNER_TYPES.has(t) && !config.staticOwnerTypes.has(t));
44
+ // Explicit empty Set is the opt-out signal; don't second-guess it.
45
+ if (missing.length > 0 && config.staticOwnerTypes.size > 0) {
46
+ throw new Error(`[MethodExtractionConfig:${config.language}] typeDeclarationNodes includes static-implying owner type(s) ` +
47
+ `${JSON.stringify(missing)} that are missing from staticOwnerTypes. ` +
48
+ `Either add them to staticOwnerTypes, or set 'staticOwnerTypes: new Set()' to opt out explicitly.`);
49
+ }
50
+ }
8
51
  const typeDeclarationSet = new Set(config.typeDeclarationNodes);
9
52
  const methodNodeSet = new Set(config.methodNodeTypes);
10
53
  const bodyNodeSet = new Set(config.bodyNodeTypes);
@@ -137,8 +180,9 @@ function buildMethod(node, ownerNode, context, config) {
137
180
  // Domain invariant: abstract methods cannot be final
138
181
  if (isAbstract)
139
182
  isFinal = false;
140
- // companion_object / object_declaration members are effectively static on JVM
141
- const isStatic = STATIC_OWNER_TYPES.has(ownerNode.type) || config.isStatic(node);
183
+ // Static-owner detection is config-driven: each language declares which
184
+ // container node types imply static (e.g. Ruby singleton_class, Kotlin companion_object).
185
+ const isStatic = (config.staticOwnerTypes?.has(ownerNode.type) ?? false) || config.isStatic(node);
142
186
  return {
143
187
  name,
144
188
  receiverType: config.extractReceiverType?.(node) ?? null,
@@ -73,6 +73,10 @@ export interface MethodExtractionConfig {
73
73
  isAsync?: (node: SyntaxNode) => boolean;
74
74
  isPartial?: (node: SyntaxNode) => boolean;
75
75
  isConst?: (node: SyntaxNode) => boolean;
76
+ /** Owner node types where member functions are effectively static (e.g.
77
+ * Ruby singleton_class, Kotlin companion_object / object_declaration).
78
+ * When the ownerNode matches one of these types, isStatic is forced true. */
79
+ staticOwnerTypes?: ReadonlySet<string>;
76
80
  /** Resolve the owner name from a standalone method node (e.g. Go receiver type). */
77
81
  extractOwnerName?: (node: SyntaxNode) => string | undefined;
78
82
  /** Extract a primary constructor from the owner node itself (e.g. C# 12 class Point(int x, int y)). */
@@ -47,63 +47,118 @@ function gatherAncestors(classId, parentMap) {
47
47
  export function c3Linearize(classId, parentMap, cache, inProgress) {
48
48
  if (cache.has(classId))
49
49
  return cache.get(classId);
50
- // Cycle detection: if we're already computing this class, the hierarchy is cyclic
50
+ // Iterative C3 linearization using an explicit work stack. The recursive
51
+ // version overflows the call stack on deep class hierarchies (10K+
52
+ // levels in large Android/Java codebases).
53
+ //
54
+ // Strategy: maintain a stack of { classId, phase } frames. Each frame
55
+ // goes through two phases:
56
+ // ENTER (0) – check cache / cycle, push parent frames to compute first
57
+ // MERGE (1) – all parent linearizations are cached, merge them C3-style
51
58
  const visiting = inProgress ?? new Set();
52
- if (visiting.has(classId)) {
53
- cache.set(classId, null);
54
- return null;
55
- }
56
- visiting.add(classId);
57
- const directParents = parentMap.get(classId);
58
- if (!directParents || directParents.length === 0) {
59
- visiting.delete(classId);
60
- cache.set(classId, []);
61
- return [];
62
- }
63
- // Compute linearization for each parent first
64
- const parentLinearizations = [];
65
- for (const pid of directParents) {
66
- const pLin = c3Linearize(pid, parentMap, cache, visiting);
67
- if (pLin === null) {
68
- visiting.delete(classId);
69
- cache.set(classId, null);
70
- return null;
71
- }
72
- parentLinearizations.push([pid, ...pLin]);
73
- }
74
- // Add the direct parents list as the final sequence
75
- const sequences = [...parentLinearizations, [...directParents]];
76
- const result = [];
77
- while (sequences.some((s) => s.length > 0)) {
78
- // Find a good head: one that doesn't appear in the tail of any other sequence
79
- let head = null;
80
- for (const seq of sequences) {
81
- if (seq.length === 0)
59
+ const ENTER = 0;
60
+ const MERGE = 1;
61
+ const stack = [{ id: classId, phase: ENTER }];
62
+ while (stack.length > 0) {
63
+ const frame = stack[stack.length - 1];
64
+ if (frame.phase === ENTER) {
65
+ // ── ENTER phase ─────────────────────────────────────────────
66
+ if (cache.has(frame.id)) {
67
+ stack.pop();
68
+ continue;
69
+ }
70
+ if (visiting.has(frame.id)) {
71
+ // Cycle detected
72
+ cache.set(frame.id, null);
73
+ stack.pop();
74
+ continue;
75
+ }
76
+ visiting.add(frame.id);
77
+ const directParents = parentMap.get(frame.id);
78
+ if (!directParents || directParents.length === 0) {
79
+ visiting.delete(frame.id);
80
+ cache.set(frame.id, []);
81
+ stack.pop();
82
+ continue;
83
+ }
84
+ // Switch to MERGE phase and push parents that still need computing
85
+ frame.phase = MERGE;
86
+ let allParentsCached = true;
87
+ for (let i = directParents.length - 1; i >= 0; i--) {
88
+ const pid = directParents[i];
89
+ if (!cache.has(pid)) {
90
+ stack.push({ id: pid, phase: ENTER });
91
+ allParentsCached = false;
92
+ }
93
+ }
94
+ // If all parents are already cached, proceed directly to the MERGE
95
+ // phase below (frame.phase is already MERGE, frame is at stack top).
96
+ // Otherwise, loop back to process the newly-pushed parent frames first.
97
+ if (!allParentsCached) {
82
98
  continue;
83
- const candidate = seq[0];
84
- const inTail = sequences.some((other) => other.length > 1 && other.indexOf(candidate, 1) !== -1);
85
- if (!inTail) {
86
- head = candidate;
99
+ }
100
+ }
101
+ // ── MERGE phase ───────────────────────────────────────────────
102
+ // directParents is guaranteed non-empty here — the ENTER phase already
103
+ // handles the empty-parents case and pops the frame before switching
104
+ // to MERGE.
105
+ stack.pop();
106
+ const directParents = parentMap.get(frame.id);
107
+ // Build parent linearizations from cache
108
+ const parentLinearizations = [];
109
+ let failed = false;
110
+ for (const pid of directParents) {
111
+ const pLin = cache.get(pid);
112
+ if (pLin === undefined) {
113
+ // Should not happen if phases are ordered correctly, but guard anyway
114
+ failed = true;
115
+ break;
116
+ }
117
+ if (pLin === null) {
118
+ // Parent linearization failed (cycle or inconsistent)
119
+ failed = true;
87
120
  break;
88
121
  }
122
+ parentLinearizations.push([pid, ...pLin]);
89
123
  }
90
- if (head === null) {
91
- // Inconsistent hierarchy
92
- visiting.delete(classId);
93
- cache.set(classId, null);
94
- return null;
124
+ if (failed) {
125
+ visiting.delete(frame.id);
126
+ cache.set(frame.id, null);
127
+ continue;
95
128
  }
96
- result.push(head);
97
- // Remove the chosen head from all sequences
98
- for (const seq of sequences) {
99
- if (seq.length > 0 && seq[0] === head) {
100
- seq.shift();
129
+ // Add the direct parents list as the final sequence
130
+ const sequences = [...parentLinearizations, [...directParents]];
131
+ const result = [];
132
+ let inconsistent = false;
133
+ while (sequences.some((s) => s.length > 0)) {
134
+ // Find a good head: one that doesn't appear in the tail of any other sequence
135
+ let head = null;
136
+ for (const seq of sequences) {
137
+ if (seq.length === 0)
138
+ continue;
139
+ const candidate = seq[0];
140
+ const inTail = sequences.some((other) => other.length > 1 && other.indexOf(candidate, 1) !== -1);
141
+ if (!inTail) {
142
+ head = candidate;
143
+ break;
144
+ }
145
+ }
146
+ if (head === null) {
147
+ inconsistent = true;
148
+ break;
149
+ }
150
+ result.push(head);
151
+ // Remove the chosen head from all sequences
152
+ for (const seq of sequences) {
153
+ if (seq.length > 0 && seq[0] === head) {
154
+ seq.shift();
155
+ }
101
156
  }
102
157
  }
158
+ visiting.delete(frame.id);
159
+ cache.set(frame.id, inconsistent ? null : result);
103
160
  }
104
- visiting.delete(classId);
105
- cache.set(classId, result);
106
- return result;
161
+ return cache.get(classId) ?? null;
107
162
  }
108
163
  // `gatherAncestors` is exported so mro-processor.ts can reuse the same
109
164
  // BFS traversal for graph-level MRO emission.
@@ -7,7 +7,7 @@
7
7
  * - A nested SymbolTable (file + callable name indexes) wrapped so
8
8
  * that `add()` fans out into the registries via the dispatch table
9
9
  *
10
- * ## DAG direction
10
+ * ## Dependency direction
11
11
  *
12
12
  * gitnexus-shared (NodeLabel) — leaf
13
13
  * ↑
@@ -7,7 +7,7 @@
7
7
  * - A nested SymbolTable (file + callable name indexes) wrapped so
8
8
  * that `add()` fans out into the registries via the dispatch table
9
9
  *
10
- * ## DAG direction
10
+ * ## Dependency direction
11
11
  *
12
12
  * gitnexus-shared (NodeLabel) — leaf
13
13
  * ↑
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Symbol Table — file-indexed + callable-name symbol storage.
3
3
  *
4
- * This module is a PURE LEAF in the ingestion DAG. It owns two orthogonal
5
- * O(1) indexes:
4
+ * This module is a PURE LEAF in the ingestion dependency hierarchy. It owns
5
+ * two orthogonal O(1) indexes:
6
6
  *
7
7
  * 1. fileIndex — Map<filePath, Map<name, SymbolDefinition[]>>
8
8
  * for same-file lookups (Tier 1 resolution)
@@ -10,12 +10,12 @@
10
10
  * for name-keyed callable lookups (Tier 3 widen)
11
11
  *
12
12
  * SymbolTable deliberately knows NOTHING about the owner-scoped registries
13
- * (types, methods, fields) that sit above it in the DAG. Those registries
14
- * live in `model/` and depend on SymbolTable, not the other way around.
15
- * {@link createSemanticModel} composes this pure SymbolTable with the
13
+ * (types, methods, fields) that sit above it in the dependency graph. Those
14
+ * registries live in `model/` and depend on SymbolTable, not the other way
15
+ * around. {@link createSemanticModel} composes this pure SymbolTable with the
16
16
  * registries and wraps `add()` to fan out registrations into both layers.
17
17
  *
18
- * DAG direction (strictly enforced):
18
+ * Dependency direction (strictly enforced):
19
19
  *
20
20
  * gitnexus-shared (NodeLabel) — leaf type
21
21
  * ↑
@@ -31,7 +31,7 @@
31
31
  *
32
32
  * No arrow ever points downward from this file. If you are tempted to
33
33
  * import from `./model/` here, you are going the wrong way — move the
34
- * logic up the DAG instead.
34
+ * logic up the dependency chain instead.
35
35
  */
36
36
  import type { NodeLabel } from '../../../_shared/index.js';
37
37
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Symbol Table — file-indexed + callable-name symbol storage.
3
3
  *
4
- * This module is a PURE LEAF in the ingestion DAG. It owns two orthogonal
5
- * O(1) indexes:
4
+ * This module is a PURE LEAF in the ingestion dependency hierarchy. It owns
5
+ * two orthogonal O(1) indexes:
6
6
  *
7
7
  * 1. fileIndex — Map<filePath, Map<name, SymbolDefinition[]>>
8
8
  * for same-file lookups (Tier 1 resolution)
@@ -10,12 +10,12 @@
10
10
  * for name-keyed callable lookups (Tier 3 widen)
11
11
  *
12
12
  * SymbolTable deliberately knows NOTHING about the owner-scoped registries
13
- * (types, methods, fields) that sit above it in the DAG. Those registries
14
- * live in `model/` and depend on SymbolTable, not the other way around.
15
- * {@link createSemanticModel} composes this pure SymbolTable with the
13
+ * (types, methods, fields) that sit above it in the dependency graph. Those
14
+ * registries live in `model/` and depend on SymbolTable, not the other way
15
+ * around. {@link createSemanticModel} composes this pure SymbolTable with the
16
16
  * registries and wraps `add()` to fan out registrations into both layers.
17
17
  *
18
- * DAG direction (strictly enforced):
18
+ * Dependency direction (strictly enforced):
19
19
  *
20
20
  * gitnexus-shared (NodeLabel) — leaf type
21
21
  * ↑
@@ -31,7 +31,7 @@
31
31
  *
32
32
  * No arrow ever points downward from this file. If you are tempted to
33
33
  * import from `./model/` here, you are going the wrong way — move the
34
- * logic up the DAG instead.
34
+ * logic up the dependency chain instead.
35
35
  */
36
36
  /**
37
37
  * Class-like NodeLabels — used for qualifiedName fallback inside
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MRO (Method Resolution Order) Processor
3
3
  *
4
- * Walks the inheritance DAG (EXTENDS/IMPLEMENTS edges), collects methods from
4
+ * Walks the inheritance graph (EXTENDS/IMPLEMENTS edges), collects methods from
5
5
  * each ancestor via HAS_METHOD edges, detects method-name collisions across
6
6
  * parents, and applies language-specific resolution rules to emit METHOD_OVERRIDES edges.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MRO (Method Resolution Order) Processor
3
3
  *
4
- * Walks the inheritance DAG (EXTENDS/IMPLEMENTS edges), collects methods from
4
+ * Walks the inheritance graph (EXTENDS/IMPLEMENTS edges), collects methods from
5
5
  * each ancestor via HAS_METHOD edges, detects method-name collisions across
6
6
  * parents, and applies language-specific resolution rules to emit METHOD_OVERRIDES edges.
7
7
  *