diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/c-cpp.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/c-cpp.ts new file mode 100644 index 000000000..304851d00 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/c-cpp.ts @@ -0,0 +1,25 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/c-cpp.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * C heritage extraction config. + * + * C has no class inheritance — no heritage captures expected. + * Config exists for completeness in the provider wiring. + */ +export const cHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.C, +}; + +/** + * C++ heritage extraction config. + * + * C++ has standard extends heritage through tree-sitter captures + * (class A : public B). Multiple inheritance uses leftmost-base MRO + * at the resolution layer. No special extraction hooks needed. + */ +export const cppHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.CPlusPlus, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/csharp.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/csharp.ts new file mode 100644 index 000000000..4363c8307 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/csharp.ts @@ -0,0 +1,15 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/csharp.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * C# heritage extraction config. + * + * C# has standard extends/implements heritage through tree-sitter + * captures. Interface detection uses I-prefix convention (handled + * by heritage resolution strategy, not here). + */ +export const csharpHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.CSharp, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/dart.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/dart.ts new file mode 100644 index 000000000..0b3907267 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/dart.ts @@ -0,0 +1,14 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/dart.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * Dart heritage extraction config. + * + * Dart has standard extends/implements/with heritage through tree-sitter + * captures. No special extraction hooks needed. + */ +export const dartHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Dart, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts new file mode 100644 index 000000000..df13b2a39 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts @@ -0,0 +1,27 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * Go heritage extraction config. + * + * Go struct embedding: the tree-sitter query matches ALL field_declarations + * with type_identifier, but only anonymous fields (no name) are embedded. + * Named fields like `Breed string` also match — skip them. + * + * The shouldSkipExtends hook checks if the extends node's parent is a + * field_declaration with a named field child, indicating a regular + * (non-embedded) field that should not produce a heritage record. + */ +export const goHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Go, + + shouldSkipExtends(extendsNode) { + const fieldDecl = extendsNode.parent; + return ( + fieldDecl?.type === 'field_declaration' && + fieldDecl.childForFieldName?.('name') != null + ); + }, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/jvm.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/jvm.ts new file mode 100644 index 000000000..006cd2cb3 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/jvm.ts @@ -0,0 +1,24 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/jvm.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * Java heritage extraction config. + * + * Java has standard extends/implements heritage through tree-sitter + * captures. No special extraction hooks needed. + */ +export const javaHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Java, +}; + +/** + * Kotlin heritage extraction config. + * + * Kotlin uses ':' for both extends and implements (delegation markers + * are tree-sitter captures). No special extraction hooks needed. + */ +export const kotlinHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Kotlin, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/php.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/php.ts new file mode 100644 index 000000000..6843712de --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/php.ts @@ -0,0 +1,14 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/php.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * PHP heritage extraction config. + * + * PHP has standard extends/implements heritage through tree-sitter + * captures. No special extraction hooks needed. + */ +export const phpHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.PHP, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/python.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/python.ts new file mode 100644 index 000000000..cabcdd78f --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/python.ts @@ -0,0 +1,15 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/python.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * Python heritage extraction config. + * + * Python has standard extends heritage through tree-sitter captures + * (class A(B, C)). Multiple inheritance is handled by C3 MRO at + * the resolution layer. No special extraction hooks needed. + */ +export const pythonHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Python, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts new file mode 100644 index 000000000..c28324a87 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts @@ -0,0 +1,72 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig, HeritageInfo } from '../../heritage-types.js'; +import type { SyntaxNode } from '../../utils/ast-helpers.js'; + +/** + * Maximum parent depth for enclosing class/module walk. + * Prevents runaway walks on malformed/deeply-nested ASTs. + */ +const MAX_PARENT_DEPTH = 50; + +/** + * Walk up the AST from a call node to find the enclosing class or module name. + * Ruby include/extend/prepend calls must be inside a class or module body. + */ +function findEnclosingClassName(callNode: SyntaxNode): string | null { + let current = callNode.parent; + let depth = 0; + while (current && ++depth <= MAX_PARENT_DEPTH) { + if (current.type === 'class' || current.type === 'module') { + const nameNode = current.childForFieldName?.('name'); + if (nameNode) return nameNode.text; + } + current = current.parent; + } + return null; +} + +/** Ruby heritage call names that express mixin inclusion. */ +const RUBY_HERITAGE_CALL_NAMES: ReadonlySet = new Set([ + 'include', + 'extend', + 'prepend', +]); + +/** + * Ruby heritage extraction config. + * + * Ruby has no tree-sitter heritage captures — inheritance is expressed + * through the class definition syntax (`class A < B`) which produces + * standard @heritage.extends captures, and through method calls + * (include/extend/prepend) which are call-based heritage. + * + * The callBasedHeritage hook absorbs the mixin routing logic that was + * previously in call-routing.ts (routeRubyCall). + */ +export const rubyHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Ruby, + + callBasedHeritage: { + callNames: RUBY_HERITAGE_CALL_NAMES, + + extract(calledName, callNode, _filePath): HeritageInfo[] { + const enclosingClass = findEnclosingClassName(callNode); + if (!enclosingClass) return []; + + const results: HeritageInfo[] = []; + const argList = callNode.childForFieldName?.('arguments'); + for (const arg of argList?.children ?? []) { + if (arg.type === 'constant' || arg.type === 'scope_resolution') { + results.push({ + className: enclosingClass, + parentName: arg.text, + kind: calledName, // 'include' | 'extend' | 'prepend' + }); + } + } + return results; + }, + }, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/rust.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/rust.ts new file mode 100644 index 000000000..f4c6b9d26 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/rust.ts @@ -0,0 +1,15 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/rust.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * Rust heritage extraction config. + * + * Rust uses impl Trait for Struct syntax. Heritage captures include + * heritage.trait for trait implementations. No special extraction + * hooks needed beyond the standard capture handling. + */ +export const rustHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Rust, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/swift.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/swift.ts new file mode 100644 index 000000000..058954db4 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/swift.ts @@ -0,0 +1,16 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/swift.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * Swift heritage extraction config. + * + * Swift uses ':' for protocol conformance and class inheritance. + * The heritageDefaultEdge ('IMPLEMENTS') and resolution strategy + * are handled at the provider/resolution layer. No special + * extraction hooks needed. + */ +export const swiftHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.Swift, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/configs/typescript-javascript.ts b/gitnexus/src/core/ingestion/heritage-extractors/configs/typescript-javascript.ts new file mode 100644 index 000000000..e59754b36 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/configs/typescript-javascript.ts @@ -0,0 +1,24 @@ +// gitnexus/src/core/ingestion/heritage-extractors/configs/typescript-javascript.ts + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { HeritageExtractionConfig } from '../../heritage-types.js'; + +/** + * TypeScript heritage extraction config. + * + * TypeScript has standard extends/implements heritage through tree-sitter + * captures. No special extraction hooks needed. + */ +export const typescriptHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.TypeScript, +}; + +/** + * JavaScript heritage extraction config. + * + * JavaScript has extends heritage (class A extends B). No implements + * keyword in plain JS. No special extraction hooks needed. + */ +export const javascriptHeritageConfig: HeritageExtractionConfig = { + language: SupportedLanguages.JavaScript, +}; diff --git a/gitnexus/src/core/ingestion/heritage-extractors/generic.ts b/gitnexus/src/core/ingestion/heritage-extractors/generic.ts new file mode 100644 index 000000000..9f12b7665 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-extractors/generic.ts @@ -0,0 +1,73 @@ +// gitnexus/src/core/ingestion/heritage-extractors/generic.ts + +/** + * Generic table-driven heritage extractor factory. + * + * Follows the same config+factory pattern as method-extractors/generic.ts, + * field-extractors/generic.ts, call-extractors/generic.ts, and + * variable-extractors/generic.ts. + * + * Define a HeritageExtractionConfig per language and generate extractors + * from configs. The factory creates a HeritageExtractor whose behaviour + * is entirely driven by HeritageExtractionConfig. + */ + +import type { CaptureMap } from '../language-provider.js'; +import type { + HeritageExtractionConfig, + HeritageExtractor, + HeritageExtractorContext, + HeritageInfo, +} from '../heritage-types.js'; +import type { SyntaxNode } from '../utils/ast-helpers.js'; + +/** + * Create a HeritageExtractor from a declarative config. + */ +export function createHeritageExtractor(config: HeritageExtractionConfig): HeritageExtractor { + const callNameSet = config.callBasedHeritage?.callNames; + + return { + language: config.language, + + extract(captureMap: CaptureMap, context: HeritageExtractorContext): HeritageInfo[] { + const classNode = captureMap['heritage.class']; + if (!classNode) return []; + + const className = classNode.text; + const results: HeritageInfo[] = []; + + const extendsNode = captureMap['heritage.extends']; + if (extendsNode) { + if (!config.shouldSkipExtends?.(extendsNode)) { + results.push({ className, parentName: extendsNode.text, kind: 'extends' }); + } + } + + const implementsNode = captureMap['heritage.implements']; + if (implementsNode) { + results.push({ className, parentName: implementsNode.text, kind: 'implements' }); + } + + const traitNode = captureMap['heritage.trait']; + if (traitNode) { + results.push({ className, parentName: traitNode.text, kind: 'trait-impl' }); + } + + return results; + }, + + ...(callNameSet + ? { + extractFromCall( + calledName: string, + callNode: SyntaxNode, + context: HeritageExtractorContext, + ): HeritageInfo[] | null { + if (!callNameSet.has(calledName)) return null; + return config.callBasedHeritage!.extract(calledName, callNode, context.filePath); + }, + } + : {}), + }; +} diff --git a/gitnexus/src/core/ingestion/heritage-processor.ts b/gitnexus/src/core/ingestion/heritage-processor.ts index bc8628fa5..d3921a6ec 100644 --- a/gitnexus/src/core/ingestion/heritage-processor.ts +++ b/gitnexus/src/core/ingestion/heritage-processor.ts @@ -407,36 +407,20 @@ export async function extractExtractedHeritageFromFiles( }); if (captureMap['heritage.class']) { - if (captureMap['heritage.extends']) { - const extendsNode = captureMap['heritage.extends']; - const fieldDecl = extendsNode.parent; - const isNamedField = - fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'); - if (!isNamedField) { + if (provider.heritageExtractor) { + const heritageItems = provider.heritageExtractor.extract(captureMap, { + filePath: file.path, + language, + }); + for (const item of heritageItems) { out.push({ filePath: file.path, - className: captureMap['heritage.class'].text, - parentName: captureMap['heritage.extends'].text, - kind: 'extends', + className: item.className, + parentName: item.parentName, + kind: item.kind, }); } } - if (captureMap['heritage.implements']) { - out.push({ - filePath: file.path, - className: captureMap['heritage.class'].text, - parentName: captureMap['heritage.implements'].text, - kind: 'implements', - }); - } - if (captureMap['heritage.trait']) { - out.push({ - filePath: file.path, - className: captureMap['heritage.class'].text, - parentName: captureMap['heritage.trait'].text, - kind: 'trait-impl', - }); - } } } } diff --git a/gitnexus/src/core/ingestion/heritage-types.ts b/gitnexus/src/core/ingestion/heritage-types.ts new file mode 100644 index 000000000..73506f9d4 --- /dev/null +++ b/gitnexus/src/core/ingestion/heritage-types.ts @@ -0,0 +1,108 @@ +// gitnexus/src/core/ingestion/heritage-types.ts + +/** + * Types for the language-agnostic heritage extraction pipeline. + * + * Follows the same pattern as call-types.ts, variable-types.ts, and + * method-types.ts: defines the domain interfaces consumed by + * createHeritageExtractor() and the per-language configs. + * + * Heritage extraction handles extends/implements/trait-impl captures from + * tree-sitter queries, plus call-based heritage for languages like Ruby + * (include/extend/prepend expressed as method calls). + */ + +import type { SupportedLanguages } from 'gitnexus-shared'; +import type { SyntaxNode } from './utils/ast-helpers.js'; +import type { CaptureMap } from './language-provider.js'; + +// --------------------------------------------------------------------------- +// Extracted result +// --------------------------------------------------------------------------- + +/** + * Per-match heritage extraction result. The parse worker adds filePath to + * produce the final {@link ExtractedHeritage} that enters the resolution + * pipeline (heritage-processor.ts / heritage-map.ts). + */ +export interface HeritageInfo { + className: string; + parentName: string; + /** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */ + kind: string; +} + +// --------------------------------------------------------------------------- +// Context +// --------------------------------------------------------------------------- + +export interface HeritageExtractorContext { + filePath: string; + language: SupportedLanguages; +} + +// --------------------------------------------------------------------------- +// Extractor interface (produced by createHeritageExtractor) +// --------------------------------------------------------------------------- + +export interface HeritageExtractor { + readonly language: SupportedLanguages; + + /** + * Extract heritage records from tree-sitter @heritage.* captures. + * + * @param captureMap The capture map from a single tree-sitter match + * @param context File path and language context + * @returns Array of heritage records (may be empty if captures don't match) + */ + extract(captureMap: CaptureMap, context: HeritageExtractorContext): HeritageInfo[]; + + /** + * Extract heritage from a call node (for languages where heritage is + * expressed as method calls, e.g., Ruby include/extend/prepend). + * + * @param calledName The method name (e.g. 'include', 'extend', 'prepend') + * @param callNode The tree-sitter call AST node + * @param context File path and language context + * @returns Heritage records if the call is heritage-related, or null to + * fall through to the call router / normal call handling. + */ + extractFromCall?( + calledName: string, + callNode: SyntaxNode, + context: HeritageExtractorContext, + ): HeritageInfo[] | null; +} + +// --------------------------------------------------------------------------- +// Config interface (one per language / language group) +// --------------------------------------------------------------------------- + +export interface HeritageExtractionConfig { + language: SupportedLanguages; + + /** + * Called for heritage.extends captures. Return true to skip this extends + * capture. Used by Go to skip named struct fields that match the + * field_declaration pattern but are not anonymous embeddings. + * + * Default: never skip (all extends captures are valid). + */ + shouldSkipExtends?: (extendsNode: SyntaxNode) => boolean; + + /** + * Call-based heritage extraction for languages where heritage is expressed + * as method calls (e.g., Ruby include/extend/prepend). + * + * callNames: set of method names that trigger heritage extraction. + * extract: extract heritage items from the call node + method name. + */ + callBasedHeritage?: { + readonly callNames: ReadonlySet; + extract( + calledName: string, + callNode: SyntaxNode, + filePath: string, + ): HeritageInfo[]; + }; +} diff --git a/gitnexus/src/core/ingestion/language-provider.ts b/gitnexus/src/core/ingestion/language-provider.ts index 298c7999f..5b6479111 100644 --- a/gitnexus/src/core/ingestion/language-provider.ts +++ b/gitnexus/src/core/ingestion/language-provider.ts @@ -16,6 +16,7 @@ import type { CallExtractor } from './call-types.js'; import type { ClassExtractor } from './class-types.js'; import type { ExportChecker } from './export-detection.js'; import type { FieldExtractor } from './field-extractor.js'; +import type { HeritageExtractor } from './heritage-types.js'; import type { MethodExtractor } from './method-types.js'; import type { VariableExtractor } from './variable-types.js'; import type { ImportResolverFn } from './import-resolvers/types.js'; @@ -179,6 +180,11 @@ interface LanguageProviderConfig { * Uses the same provider-driven strategy pattern as method/field extraction so * namespace/package/module rules stay language-specific. */ readonly classExtractor?: ClassExtractor; + /** Heritage extractor for extracting extends/implements/trait-impl relationships + * from tree-sitter @heritage.* captures and call-based heritage (e.g., Ruby + * include/extend/prepend). Produced by createHeritageExtractor() with a + * per-language HeritageExtractionConfig. Default: undefined (inline fallback). */ + readonly heritageExtractor?: HeritageExtractor; /** Extract a semantic description for a definition node (e.g., PHP Eloquent * property arrays, relation method descriptions). * Default: undefined (no description extraction). */ diff --git a/gitnexus/src/core/ingestion/languages/c-cpp.ts b/gitnexus/src/core/ingestion/languages/c-cpp.ts index 5a60959a0..e14f61cc2 100644 --- a/gitnexus/src/core/ingestion/languages/c-cpp.ts +++ b/gitnexus/src/core/ingestion/languages/c-cpp.ts @@ -43,6 +43,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { cVariableConfig, cppVariableConfig } from '../variable-extractors/configs/c-cpp.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { cHeritageConfig, cppHeritageConfig } from '../heritage-extractors/configs/c-cpp.js'; const C_BUILT_INS: ReadonlySet = new Set([ 'printf', @@ -329,6 +331,7 @@ export const cProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(cVariableConfig), classExtractor: cClassExtractor, + heritageExtractor: createHeritageExtractor(cHeritageConfig), labelOverride: cppLabelOverride, builtInNames: C_BUILT_INS, }); @@ -350,6 +353,7 @@ export const cppProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(cppVariableConfig), classExtractor: cppClassExtractor, + heritageExtractor: createHeritageExtractor(cppHeritageConfig), labelOverride: cppLabelOverride, builtInNames: C_BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/csharp.ts b/gitnexus/src/core/ingestion/languages/csharp.ts index 9a447fced..51fc96e67 100644 --- a/gitnexus/src/core/ingestion/languages/csharp.ts +++ b/gitnexus/src/core/ingestion/languages/csharp.ts @@ -24,6 +24,8 @@ import { createMethodExtractor } from '../method-extractors/generic.js'; import { csharpMethodConfig } from '../method-extractors/configs/csharp.js'; import { createVariableExtractor } from '../variable-extractors/generic.js'; import { csharpVariableConfig } from '../variable-extractors/configs/csharp.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { csharpHeritageConfig } from '../heritage-extractors/configs/csharp.js'; const BUILT_INS: ReadonlySet = new Set([ 'Console', @@ -135,5 +137,6 @@ export const csharpProvider = defineLanguage({ methodExtractor: createMethodExtractor(csharpMethodConfig), variableExtractor: createVariableExtractor(csharpVariableConfig), classExtractor: createClassExtractor(csharpClassConfig), + heritageExtractor: createHeritageExtractor(csharpHeritageConfig), builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/dart.ts b/gitnexus/src/core/ingestion/languages/dart.ts index a596d35ae..311175834 100644 --- a/gitnexus/src/core/ingestion/languages/dart.ts +++ b/gitnexus/src/core/ingestion/languages/dart.ts @@ -30,6 +30,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { dartVariableConfig } from '../variable-extractors/configs/dart.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { dartCallConfig } from '../call-extractors/configs/dart.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { dartHeritageConfig } from '../heritage-extractors/configs/dart.js'; /** * Resolve the enclosing function from a `function_body` node by looking at its @@ -102,6 +104,7 @@ export const dartProvider = defineLanguage({ methodExtractor: createMethodExtractor(dartMethodConfig), variableExtractor: createVariableExtractor(dartVariableConfig), classExtractor: createClassExtractor(dartClassConfig), + heritageExtractor: createHeritageExtractor(dartHeritageConfig), enclosingFunctionFinder: dartEnclosingFunctionFinder, builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/go.ts b/gitnexus/src/core/ingestion/languages/go.ts index dcb0ec3d0..245dc197b 100644 --- a/gitnexus/src/core/ingestion/languages/go.ts +++ b/gitnexus/src/core/ingestion/languages/go.ts @@ -26,6 +26,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { goVariableConfig } from '../variable-extractors/configs/go.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { goCallConfig } from '../call-extractors/configs/go.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { goHeritageConfig } from '../heritage-extractors/configs/go.js'; export const goProvider = defineLanguage({ id: SupportedLanguages.Go, @@ -40,4 +42,5 @@ export const goProvider = defineLanguage({ methodExtractor: createMethodExtractor(goMethodConfig), variableExtractor: createVariableExtractor(goVariableConfig), classExtractor: createClassExtractor(goClassConfig), + heritageExtractor: createHeritageExtractor(goHeritageConfig), }); diff --git a/gitnexus/src/core/ingestion/languages/java.ts b/gitnexus/src/core/ingestion/languages/java.ts index f905213c8..1fbfbe240 100644 --- a/gitnexus/src/core/ingestion/languages/java.ts +++ b/gitnexus/src/core/ingestion/languages/java.ts @@ -25,6 +25,8 @@ import { createMethodExtractor } from '../method-extractors/generic.js'; import { javaMethodConfig } from '../method-extractors/configs/jvm.js'; import { createVariableExtractor } from '../variable-extractors/generic.js'; import { javaVariableConfig } from '../variable-extractors/configs/jvm.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { javaHeritageConfig } from '../heritage-extractors/configs/jvm.js'; export const javaProvider = defineLanguage({ id: SupportedLanguages.Java, @@ -41,4 +43,5 @@ export const javaProvider = defineLanguage({ methodExtractor: createMethodExtractor(javaMethodConfig), variableExtractor: createVariableExtractor(javaVariableConfig), classExtractor: createClassExtractor(javaClassConfig), + heritageExtractor: createHeritageExtractor(javaHeritageConfig), }); diff --git a/gitnexus/src/core/ingestion/languages/kotlin.ts b/gitnexus/src/core/ingestion/languages/kotlin.ts index 0f8a73425..ea50cc29a 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin.ts @@ -27,6 +27,8 @@ import { createMethodExtractor } from '../method-extractors/generic.js'; import { kotlinMethodConfig } from '../method-extractors/configs/jvm.js'; import { createVariableExtractor } from '../variable-extractors/generic.js'; import { kotlinVariableConfig } from '../variable-extractors/configs/jvm.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { kotlinHeritageConfig } from '../heritage-extractors/configs/jvm.js'; /** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method). * Kotlin grammar uses function_declaration for both top-level functions and class methods. @@ -116,6 +118,7 @@ export const kotlinProvider = defineLanguage({ methodExtractor: createMethodExtractor(kotlinMethodConfig), variableExtractor: createVariableExtractor(kotlinVariableConfig), classExtractor: createClassExtractor(kotlinClassConfig), + heritageExtractor: createHeritageExtractor(kotlinHeritageConfig), builtInNames: BUILT_INS, labelOverride: (functionNode, defaultLabel) => { if (defaultLabel !== 'Function') return defaultLabel; diff --git a/gitnexus/src/core/ingestion/languages/php.ts b/gitnexus/src/core/ingestion/languages/php.ts index af999bb4f..001f3fd49 100644 --- a/gitnexus/src/core/ingestion/languages/php.ts +++ b/gitnexus/src/core/ingestion/languages/php.ts @@ -26,6 +26,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { phpVariableConfig } from '../variable-extractors/configs/php.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { phpCallConfig } from '../call-extractors/configs/php.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { phpHeritageConfig } from '../heritage-extractors/configs/php.js'; const BUILT_INS: ReadonlySet = new Set([ 'echo', @@ -248,6 +250,7 @@ export const phpProvider = defineLanguage({ methodExtractor: createMethodExtractor(phpMethodConfig), variableExtractor: createVariableExtractor(phpVariableConfig), classExtractor: createClassExtractor(phpClassConfig), + heritageExtractor: createHeritageExtractor(phpHeritageConfig), descriptionExtractor: phpDescriptionExtractor, isRouteFile: isPhpRouteFile, builtInNames: BUILT_INS, diff --git a/gitnexus/src/core/ingestion/languages/python.ts b/gitnexus/src/core/ingestion/languages/python.ts index 74ac72396..3c06b0bd1 100644 --- a/gitnexus/src/core/ingestion/languages/python.ts +++ b/gitnexus/src/core/ingestion/languages/python.ts @@ -28,6 +28,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { pythonVariableConfig } from '../variable-extractors/configs/python.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { pythonCallConfig } from '../call-extractors/configs/python.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { pythonHeritageConfig } from '../heritage-extractors/configs/python.js'; const BUILT_INS: ReadonlySet = new Set([ 'print', @@ -74,5 +76,6 @@ export const pythonProvider = defineLanguage({ methodExtractor: createMethodExtractor(pythonMethodConfig), variableExtractor: createVariableExtractor(pythonVariableConfig), classExtractor: createClassExtractor(pythonClassConfig), + heritageExtractor: createHeritageExtractor(pythonHeritageConfig), builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/ruby.ts b/gitnexus/src/core/ingestion/languages/ruby.ts index 081740885..5549364c9 100644 --- a/gitnexus/src/core/ingestion/languages/ruby.ts +++ b/gitnexus/src/core/ingestion/languages/ruby.ts @@ -27,6 +27,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { rubyVariableConfig } from '../variable-extractors/configs/ruby.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { rubyCallConfig } from '../call-extractors/configs/ruby.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { rubyHeritageConfig } from '../heritage-extractors/configs/ruby.js'; /** Ruby method/singleton_method: extract name from 'name' field, label as Method. */ const rubyExtractFunctionName = ( @@ -137,5 +139,6 @@ export const rubyProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(rubyVariableConfig), classExtractor: createClassExtractor(rubyClassConfig), + heritageExtractor: createHeritageExtractor(rubyHeritageConfig), builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/rust.ts b/gitnexus/src/core/ingestion/languages/rust.ts index 8086db753..70df56ad1 100644 --- a/gitnexus/src/core/ingestion/languages/rust.ts +++ b/gitnexus/src/core/ingestion/languages/rust.ts @@ -30,6 +30,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { rustVariableConfig } from '../variable-extractors/configs/rust.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { rustCallConfig } from '../call-extractors/configs/rust.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { rustHeritageConfig } from '../heritage-extractors/configs/rust.js'; /** Rust impl_item: find the function_item child and extract its name as a Method. */ const rustExtractFunctionName = ( @@ -134,5 +136,6 @@ export const rustProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(rustVariableConfig), classExtractor: createClassExtractor(rustClassConfig), + heritageExtractor: createHeritageExtractor(rustHeritageConfig), builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/swift.ts b/gitnexus/src/core/ingestion/languages/swift.ts index 450b49e6b..07777f058 100644 --- a/gitnexus/src/core/ingestion/languages/swift.ts +++ b/gitnexus/src/core/ingestion/languages/swift.ts @@ -30,6 +30,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { swiftVariableConfig } from '../variable-extractors/configs/swift.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { swiftCallConfig } from '../call-extractors/configs/swift.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { swiftHeritageConfig } from '../heritage-extractors/configs/swift.js'; /** * Group Swift files by SPM target for implicit module visibility. @@ -254,6 +256,7 @@ export const swiftProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(swiftVariableConfig), classExtractor: createClassExtractor(swiftClassConfig), + heritageExtractor: createHeritageExtractor(swiftHeritageConfig), implicitImportWirer: wireSwiftImplicitImports, builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/typescript.ts b/gitnexus/src/core/ingestion/languages/typescript.ts index 71f1b0315..018852978 100644 --- a/gitnexus/src/core/ingestion/languages/typescript.ts +++ b/gitnexus/src/core/ingestion/languages/typescript.ts @@ -43,6 +43,11 @@ import { typescriptCallConfig, javascriptCallConfig, } from '../call-extractors/configs/typescript-javascript.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { + typescriptHeritageConfig, + javascriptHeritageConfig, +} from '../heritage-extractors/configs/typescript-javascript.js'; /** * TypeScript/JavaScript: arrow_function and function_expression get their name @@ -182,6 +187,7 @@ export const typescriptProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(typescriptVariableConfig), classExtractor: createClassExtractor(typescriptClassConfig), + heritageExtractor: createHeritageExtractor(typescriptHeritageConfig), builtInNames: BUILT_INS, }); @@ -201,5 +207,6 @@ export const javascriptProvider = defineLanguage({ }), variableExtractor: createVariableExtractor(javascriptVariableConfig), classExtractor: createClassExtractor(javascriptClassConfig), + heritageExtractor: createHeritageExtractor(javascriptHeritageConfig), builtInNames: BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/languages/vue.ts b/gitnexus/src/core/ingestion/languages/vue.ts index 243d941cf..457262e8c 100644 --- a/gitnexus/src/core/ingestion/languages/vue.ts +++ b/gitnexus/src/core/ingestion/languages/vue.ts @@ -27,6 +27,8 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { typescriptVariableConfig } from '../variable-extractors/configs/typescript-javascript.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { typescriptCallConfig } from '../call-extractors/configs/typescript-javascript.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import { typescriptHeritageConfig } from '../heritage-extractors/configs/typescript-javascript.js'; const VUE_SPECIFIC_BUILT_INS = [ 'ref', @@ -76,5 +78,6 @@ export const vueProvider = defineLanguage({ fieldExtractor: typescriptFieldExtractor, variableExtractor: createVariableExtractor(typescriptVariableConfig), classExtractor: vueClassExtractor, + heritageExtractor: createHeritageExtractor(typescriptHeritageConfig), builtInNames: VUE_BUILT_INS, }); diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 130f489ac..f982788ae 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -1699,6 +1699,26 @@ const processFileGroup = ( if (callNameNode) { const calledName = callNameNode.text; + // Check heritage extractor for call-based heritage (e.g., Ruby include/extend/prepend) + if (provider.heritageExtractor?.extractFromCall) { + const heritageItems = provider.heritageExtractor.extractFromCall( + calledName, + captureMap['call']!, + { filePath: file.path, language }, + ); + if (heritageItems && heritageItems.length > 0) { + for (const item of heritageItems) { + result.heritage.push({ + filePath: file.path, + className: item.className, + parentName: item.parentName, + kind: item.kind, + }); + } + continue; + } + } + // Dispatch: route language-specific calls (heritage, properties, imports) const routed = callRouter?.(calledName, captureMap['call']); if (routed) { @@ -1878,40 +1898,24 @@ const processFileGroup = ( continue; } - // Extract heritage (extends/implements) + // Extract heritage (extends/implements) via provider heritage extractor if (captureMap['heritage.class']) { - if (captureMap['heritage.extends']) { - // Go struct embedding: the query matches ALL field_declarations with - // type_identifier, but only anonymous fields (no name) are embedded. - // Named fields like `Breed string` also match — skip them. - const extendsNode = captureMap['heritage.extends']; - const fieldDecl = extendsNode.parent; - const isNamedField = - fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'); - if (!isNamedField) { + if (provider.heritageExtractor) { + const heritageItems = provider.heritageExtractor.extract(captureMap, { + filePath: file.path, + language, + }); + for (const item of heritageItems) { result.heritage.push({ filePath: file.path, - className: captureMap['heritage.class'].text, - parentName: captureMap['heritage.extends'].text, - kind: 'extends', + className: item.className, + parentName: item.parentName, + kind: item.kind, }); } - } - if (captureMap['heritage.implements']) { - result.heritage.push({ - filePath: file.path, - className: captureMap['heritage.class'].text, - parentName: captureMap['heritage.implements'].text, - kind: 'implements', - }); - } - if (captureMap['heritage.trait']) { - result.heritage.push({ - filePath: file.path, - className: captureMap['heritage.class'].text, - parentName: captureMap['heritage.trait'].text, - kind: 'trait-impl', - }); + if (heritageItems.length > 0) { + continue; + } } if ( captureMap['heritage.extends'] ||