Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gitnexus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
1. **Structure** — Walks the file tree and maps folder/file relationships
2. **Parsing** — Extracts functions, classes, methods, and interfaces using Tree-sitter ASTs
3. **Resolution** — Resolves imports and function calls across files with language-aware logic
- **Field & Property Type Resolution** — Tracks field types across classes and interfaces for deep chain resolution (e.g., `user.address.city.getName()`)
- **Return-Type-Aware Variable Binding** — Infers variable types from function return types, enabling accurate call-result binding
4. **Clustering** — Groups related symbols into functional communities
5. **Processes** — Traces execution flows from entry points through call chains
6. **Search** — Builds hybrid search indexes for fast retrieval
Expand Down
5 changes: 5 additions & 0 deletions gitnexus/src/core/graph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export type NodeProperties = {
// Section-specific (markdown heading level, 1-6)
level?: number,
returnType?: string,
// Field/property metadata (populated by FieldExtractor)
declaredType?: string,
visibility?: string, // 'public' | 'private' | 'protected' | 'internal' etc.
isStatic?: boolean,
isReadonly?: boolean,
// Response shape (top-level keys from NextResponse.json({...}) / res.json({...}))
responseKeys?: string[],
// Error response shape (top-level keys from .json() calls with status >= 400)
Expand Down
2 changes: 1 addition & 1 deletion gitnexus/src/core/ingestion/call-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export const processCalls = async (
const importedBindings = importedBindingsMap?.get(file.path);
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes });
const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
if (typeEnv && exportedTypeMap) {
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
if (fileExports) exportedTypeMap.set(file.path, fileExports);
Expand Down
61 changes: 61 additions & 0 deletions gitnexus/src/core/ingestion/field-extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// gitnexus/src/core/ingestion/field-extractor.ts

import type { SyntaxNode } from './utils/ast-helpers.js';
import { SupportedLanguages } from '../../config/supported-languages.js';
import type {
FieldExtractorContext,
ExtractedFields,
FieldVisibility,
} from './field-types.js';

/**
* Language-specific field extractor
*/
export interface FieldExtractor {
/** Language this extractor handles */
language: SupportedLanguages;

/**
* Extract fields from a class/struct/interface declaration
*/
extract(node: SyntaxNode, context: FieldExtractorContext): ExtractedFields | null;

/**
* Check if this node represents a type declaration with fields
*/
isTypeDeclaration(node: SyntaxNode): boolean;
}

/**
* Base class for field extractors with common utilities
*/
export abstract class BaseFieldExtractor implements FieldExtractor {
abstract language: SupportedLanguages;

abstract extract(node: SyntaxNode, context: FieldExtractorContext): ExtractedFields | null;
abstract isTypeDeclaration(node: SyntaxNode): boolean;

protected normalizeType(type: string | null): string | null {
if (!type) return null;
return type.trim().replace(/\s+/g, ' ');
}

protected resolveType(typeName: string, context: FieldExtractorContext): string | null {
const { typeEnv, symbolTable, filePath } = context;

// Try to find in type environment (check file scope first)
const fileEnv = typeEnv.fileScope();
const local = fileEnv.get(typeName);
if (local) return local;

// Try symbol table lookup in current file
const symbols = symbolTable.lookupExactAll(filePath, typeName);
if (symbols.length === 1) {
return symbols[0].nodeId;
}

return typeName;
}

protected abstract extractVisibility(node: SyntaxNode): FieldVisibility;
}
121 changes: 121 additions & 0 deletions gitnexus/src/core/ingestion/field-extractors/configs/c-cpp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// gitnexus/src/core/ingestion/field-extractors/configs/c-cpp.ts

import { SupportedLanguages } from '../../../../config/supported-languages.js';
import type { FieldExtractionConfig } from '../generic.js';
import { hasKeyword } from './helpers.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { FieldVisibility } from '../../field-types.js';

/**
* Detect C++ access specifier (public:/private:/protected:) by walking
* backwards from the field node through siblings.
*/
function cppAccessSpecifier(node: SyntaxNode): FieldVisibility | undefined {
let sibling = node.previousNamedSibling;
while (sibling) {
if (sibling.type === 'access_specifier') {
const text = sibling.text.replace(':', '').trim();
if (text === 'public' || text === 'private' || text === 'protected') return text;
}
sibling = sibling.previousNamedSibling;
}
return undefined;
}

function extractFieldName(node: SyntaxNode): string | undefined {
// field_declaration > declarator:(field_identifier | pointer_declarator > field_identifier)
const declarator = node.childForFieldName('declarator');
if (declarator) {
if (declarator.type === 'field_identifier') return declarator.text;
// pointer_declarator: *fieldName
for (let i = 0; i < declarator.namedChildCount; i++) {
const child = declarator.namedChild(i);
if (child?.type === 'field_identifier') return child.text;
}
return declarator.text;
}
// fallback
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'field_identifier') return child.text;
}
return undefined;
}

function extractFieldType(node: SyntaxNode): string | undefined {
const typeNode = node.childForFieldName('type');
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
// fallback: first child that is a type node
const first = node.firstNamedChild;
if (first && (first.type === 'type_identifier' || first.type === 'primitive_type'
|| first.type === 'sized_type_specifier' || first.type === 'template_type')) {
return extractSimpleTypeName(first) ?? first.text?.trim();
}
return undefined;
}

// ---------------------------------------------------------------------------
// C++ config
// ---------------------------------------------------------------------------

export const cppConfig: FieldExtractionConfig = {
language: SupportedLanguages.CPlusPlus,
typeDeclarationNodes: [
'struct_specifier',
'class_specifier',
'union_specifier',
],
fieldNodeTypes: ['field_declaration'],
bodyNodeTypes: ['field_declaration_list'],
defaultVisibility: 'private', // C++ class default is private

extractName: extractFieldName,
extractType: extractFieldType,

extractVisibility(node) {
const access = cppAccessSpecifier(node);
if (access) return access;
// struct default = public, class default = private
const parent = node.parent?.parent;
return parent?.type === 'struct_specifier' ? 'public' : 'private';
},

isStatic(node) {
return hasKeyword(node, 'static');
},

isReadonly(node) {
return hasKeyword(node, 'const');
},
};

// ---------------------------------------------------------------------------
// C config (subset of C++)
// ---------------------------------------------------------------------------

export const cConfig: FieldExtractionConfig = {
language: SupportedLanguages.C,
typeDeclarationNodes: [
'struct_specifier',
'union_specifier',
],
fieldNodeTypes: ['field_declaration'],
bodyNodeTypes: ['field_declaration_list'],
defaultVisibility: 'public', // C structs are always public

extractName: extractFieldName,
extractType: extractFieldType,

extractVisibility(_node) {
return 'public'; // C has no access control
},

isStatic(node) {
return hasKeyword(node, 'static');
},

isReadonly(node) {
return hasKeyword(node, 'const');
},
};
80 changes: 80 additions & 0 deletions gitnexus/src/core/ingestion/field-extractors/configs/csharp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// gitnexus/src/core/ingestion/field-extractors/configs/csharp.ts

import { SupportedLanguages } from '../../../../config/supported-languages.js';
import type { FieldExtractionConfig } from '../generic.js';
import { findVisibility, hasKeyword, hasModifier } from './helpers.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { FieldVisibility } from '../../field-types.js';

const CSHARP_VIS = new Set<FieldVisibility>(['public', 'private', 'protected', 'internal']);

/**
* C# field extraction config.
*
* Handles field_declaration and property_declaration inside class/struct/interface bodies.
* The body node in tree-sitter-c-sharp is 'declaration_list'.
*/
export const csharpConfig: FieldExtractionConfig = {
language: SupportedLanguages.CSharp,
typeDeclarationNodes: [
'class_declaration',
'struct_declaration',
'interface_declaration',
'record_declaration',
],
fieldNodeTypes: ['field_declaration', 'property_declaration'],
bodyNodeTypes: ['declaration_list'],
defaultVisibility: 'private',

extractName(node) {
// field_declaration > variable_declaration > variable_declarator > identifier
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'variable_declaration') {
for (let j = 0; j < child.namedChildCount; j++) {
const declarator = child.namedChild(j);
if (declarator?.type === 'variable_declarator') {
const name = declarator.childForFieldName('name');
return name?.text ?? declarator.firstNamedChild?.text;
}
}
}
}
// property_declaration: name field
const nameNode = node.childForFieldName('name');
if (nameNode) return nameNode.text;
return undefined;
},

extractType(node) {
// field_declaration > variable_declaration > type:(predefined_type | identifier | ...)
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'variable_declaration') {
const typeNode = child.childForFieldName('type');
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
// fallback: first child that is a type
const first = child.firstNamedChild;
if (first && first.type !== 'variable_declarator') {
return extractSimpleTypeName(first) ?? first.text?.trim();
}
}
}
// property_declaration: type is first named child
const typeNode = node.childForFieldName('type');
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
return undefined;
},

extractVisibility(node) {
return findVisibility(node, CSHARP_VIS, 'private', 'modifier');
},

isStatic(node) {
return hasKeyword(node, 'static') || hasModifier(node, 'modifier', 'static');
},

isReadonly(node) {
return hasKeyword(node, 'readonly') || hasModifier(node, 'modifier', 'readonly');
},
};
81 changes: 81 additions & 0 deletions gitnexus/src/core/ingestion/field-extractors/configs/dart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// gitnexus/src/core/ingestion/field-extractors/configs/dart.ts

import { SupportedLanguages } from '../../../../config/supported-languages.js';
import type { FieldExtractionConfig } from '../generic.js';
import { hasKeyword } from './helpers.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';

/**
* Dart field extraction config.
*
* Dart class fields appear as declaration nodes inside class_body.
* Visibility is convention-based: underscore prefix = private.
*/
export const dartConfig: FieldExtractionConfig = {
language: SupportedLanguages.Dart,
typeDeclarationNodes: ['class_definition'],
fieldNodeTypes: ['declaration'],
bodyNodeTypes: ['class_body'],
defaultVisibility: 'public',

extractName(node) {
// declaration > initialized_identifier_list > initialized_identifier > identifier
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'initialized_identifier_list') {
for (let j = 0; j < child.namedChildCount; j++) {
const init = child.namedChild(j);
if (init?.type === 'initialized_identifier') {
const ident = init.firstNamedChild;
if (ident?.type === 'identifier') return ident.text;
}
}
}
if (child?.type === 'initialized_identifier') {
const ident = child.firstNamedChild;
if (ident?.type === 'identifier') return ident.text;
}
}
// fallback: look for direct identifier
const name = node.childForFieldName('name');
return name?.text;
},

extractType(node) {
// declaration > type_identifier (first named child usually)
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child && (child.type === 'type_identifier' || child.type === 'generic_type'
|| child.type === 'function_type')) {
return extractSimpleTypeName(child) ?? child.text?.trim();
}
}
return undefined;
},

extractVisibility(node) {
// Dart uses _ prefix for private
// Walk to find the identifier name
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'initialized_identifier_list') {
for (let j = 0; j < child.namedChildCount; j++) {
const init = child.namedChild(j);
if (init?.type === 'initialized_identifier') {
const ident = init.firstNamedChild;
if (ident?.text?.startsWith('_')) return 'private';
}
}
}
}
return 'public';
},

isStatic(node) {
return hasKeyword(node, 'static');
},

isReadonly(node) {
return hasKeyword(node, 'final') || hasKeyword(node, 'const');
},
};
Loading