mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat(field-extractors): generic table-driven field extractors for all 14 languages, wired into pipeline
Implements field extractors for all supported languages and integrates them into the ingestion pipeline as the single source of truth for Property node metadata. **Generic field extractor factory** — `field-extractors/generic.ts` defines a `createFieldExtractor(config)` factory that generates FieldExtractor instances from a per-language `FieldExtractionConfig`. Each config specifies AST node types, name/type/visibility extraction functions, and static/readonly detection — typically 20-40 lines per language vs 300+ for a hand-written extractor. **Per-language configs** — `field-extractors/configs/` has 11 config files covering 13 languages (TS/JS share, Java/Kotlin share). TypeScript keeps its hand-written extractor for richer handling. **LanguageProvider integration** — New optional `fieldExtractor` property on LanguageProviderConfig, set via defineLanguage() in each language file. Follows the same strategy pattern as typeConfig, exportChecker, and labelOverride. Removed the separate FieldExtractorRegistry class and field-extractors/index.ts — extractors are accessed via getProvider(lang).fieldExtractor. **Pipeline wiring** — Both parse-worker.ts (worker pool) and parsing-processor.ts (sequential fallback) now call the FieldExtractor during Property node creation. Results are cached per class node. Property nodes are enriched with: declaredType, visibility, isStatic, isReadonly. **extractPropertyDeclaredType removed** — The 100-line multi-strategy function in type-extractors/shared.ts is replaced by the FieldExtractor. All 14 languages register an extractor, eliminating the need for a generic fallback. The Python config's extractType was fixed to handle annotation-without-value patterns (address: Address). **Integration tests** — Each language's resolver test file gains pipeline-based assertions verifying visibility/isStatic/isReadonly on Property nodes via getNodesByLabelFull. Tests run through runPipelineFromRepo with real fixtures — no direct extractor calls.
This commit is contained in:
parent
d67c676c3e
commit
151e8a1333
44 changed files with 1663 additions and 201 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -25,25 +25,6 @@ export interface FieldExtractor {
|
|||
isTypeDeclaration(node: SyntaxNode): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of field extractors by language
|
||||
*/
|
||||
export class FieldExtractorRegistry {
|
||||
private extractors = new Map<SupportedLanguages, FieldExtractor>();
|
||||
|
||||
register(extractor: FieldExtractor): void {
|
||||
this.extractors.set(extractor.language, extractor);
|
||||
}
|
||||
|
||||
get(language: SupportedLanguages): FieldExtractor | undefined {
|
||||
return this.extractors.get(language);
|
||||
}
|
||||
|
||||
has(language: SupportedLanguages): boolean {
|
||||
return this.extractors.has(language);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for field extractors with common utilities
|
||||
*/
|
||||
|
|
|
|||
120
gitnexus/src/core/ingestion/field-extractors/configs/c-cpp.ts
Normal file
120
gitnexus/src/core/ingestion/field-extractors/configs/c-cpp.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// 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';
|
||||
|
||||
/**
|
||||
* Detect C++ access specifier (public:/private:/protected:) by walking
|
||||
* backwards from the field node through siblings.
|
||||
*/
|
||||
function cppAccessSpecifier(node: SyntaxNode): string | 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');
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
// 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';
|
||||
|
||||
const CSHARP_VIS = new Set(['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
gitnexus/src/core/ingestion/field-extractors/configs/dart.ts
Normal file
81
gitnexus/src/core/ingestion/field-extractors/configs/dart.ts
Normal file
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
68
gitnexus/src/core/ingestion/field-extractors/configs/go.ts
Normal file
68
gitnexus/src/core/ingestion/field-extractors/configs/go.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/go.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
|
||||
/**
|
||||
* Go field extraction config.
|
||||
*
|
||||
* Go struct fields live inside type_declaration > type_spec > struct_type >
|
||||
* field_declaration_list > field_declaration.
|
||||
*
|
||||
* Visibility in Go is based on the first character: uppercase = exported (public),
|
||||
* lowercase = unexported (package).
|
||||
*/
|
||||
export const goConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Go,
|
||||
typeDeclarationNodes: [
|
||||
'type_declaration',
|
||||
],
|
||||
fieldNodeTypes: ['field_declaration'],
|
||||
bodyNodeTypes: ['field_declaration_list'],
|
||||
defaultVisibility: 'package',
|
||||
|
||||
extractName(node) {
|
||||
// field_declaration > name:(field_identifier)
|
||||
const name = node.childForFieldName('name');
|
||||
if (name) return name.text;
|
||||
// fallback: first field_identifier child
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'field_identifier') return child.text;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// field_declaration > type:(type_identifier | pointer_type | ...)
|
||||
const typeNode = node.childForFieldName('type');
|
||||
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
// fallback: second named child is usually the type
|
||||
if (node.namedChildCount >= 2) {
|
||||
const t = node.namedChild(1);
|
||||
if (t) return extractSimpleTypeName(t) ?? t.text?.trim();
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
const name = node.childForFieldName('name');
|
||||
const text = name?.text;
|
||||
if (text && text.length > 0) {
|
||||
const first = text.charAt(0);
|
||||
return first === first.toUpperCase() && first !== first.toLowerCase()
|
||||
? 'public'
|
||||
: 'package';
|
||||
}
|
||||
return 'package';
|
||||
},
|
||||
|
||||
isStatic(_node) {
|
||||
return false; // Go has no static fields
|
||||
},
|
||||
|
||||
isReadonly(_node) {
|
||||
return false; // Go fields are not readonly
|
||||
},
|
||||
};
|
||||
145
gitnexus/src/core/ingestion/field-extractors/configs/helpers.ts
Normal file
145
gitnexus/src/core/ingestion/field-extractors/configs/helpers.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/helpers.ts
|
||||
|
||||
/**
|
||||
* Shared AST-walking helpers used by multiple language configs.
|
||||
* Keeps individual config files small.
|
||||
*/
|
||||
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modifier scanning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check whether any child of `node` (named or unnamed) has .text matching
|
||||
* one of the given `keywords`.
|
||||
*/
|
||||
export function hasKeyword(node: SyntaxNode, keyword: string): boolean {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && child.text.trim() === keyword) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a named child of type `modifierType` contains `keyword`.
|
||||
* Useful for languages that group modifiers under a wrapper node
|
||||
* (e.g. Java 'modifiers', Kotlin 'modifiers').
|
||||
*/
|
||||
export function hasModifier(node: SyntaxNode, modifierType: string, keyword: string): boolean {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === modifierType) {
|
||||
for (let j = 0; j < child.childCount; j++) {
|
||||
const mod = child.child(j);
|
||||
if (mod && mod.text.trim() === keyword) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first matching visibility keyword found either as a direct keyword
|
||||
* child or inside a modifier wrapper node.
|
||||
*/
|
||||
export function findVisibility(
|
||||
node: SyntaxNode,
|
||||
keywords: ReadonlySet<string>,
|
||||
defaultVis: string,
|
||||
modifierNodeType?: string,
|
||||
): string {
|
||||
// Direct keyword children
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && keywords.has(child.text.trim())) return child.text.trim();
|
||||
}
|
||||
// Modifier wrapper
|
||||
if (modifierNodeType) {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === modifierNodeType) {
|
||||
for (let j = 0; j < child.childCount; j++) {
|
||||
const mod = child.child(j);
|
||||
if (mod && keywords.has(mod.text.trim())) return mod.text.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultVis;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Name and type extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract the text of the first named child whose type is in `types`.
|
||||
*/
|
||||
export function firstChildText(node: SyntaxNode, types: ReadonlySet<string>): string | undefined {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && types.has(child.type)) return child.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first named child node whose type is in `types`.
|
||||
*/
|
||||
export function firstChildOfType(node: SyntaxNode, types: ReadonlySet<string>): SyntaxNode | null {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && types.has(child.type)) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type text from a named field on the node, using extractSimpleTypeName.
|
||||
* Falls back to raw .text of the field child if extractSimpleTypeName returns undefined.
|
||||
*/
|
||||
export function typeFromField(node: SyntaxNode, fieldName: string): string | undefined {
|
||||
const typeNode = node.childForFieldName(fieldName);
|
||||
if (!typeNode) return undefined;
|
||||
return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk named children looking for a type_annotation node and extract its type.
|
||||
*/
|
||||
export function typeFromAnnotation(node: SyntaxNode): string | undefined {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'type_annotation') {
|
||||
const inner = child.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner) ?? inner.text?.trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first descendant (depth-first, one level) matching one of the given types
|
||||
* and return its text via extractSimpleTypeName.
|
||||
*/
|
||||
export function typeFromDescendant(node: SyntaxNode, types: ReadonlySet<string>): string | undefined {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (types.has(child.type)) {
|
||||
return extractSimpleTypeName(child) ?? child.text?.trim();
|
||||
}
|
||||
// one more level
|
||||
for (let j = 0; j < child.namedChildCount; j++) {
|
||||
const grandchild = child.namedChild(j);
|
||||
if (grandchild && types.has(grandchild.type)) {
|
||||
return extractSimpleTypeName(grandchild) ?? grandchild.text?.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
133
gitnexus/src/core/ingestion/field-extractors/configs/jvm.ts
Normal file
133
gitnexus/src/core/ingestion/field-extractors/configs/jvm.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/jvm.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { findVisibility, hasKeyword, hasModifier, typeFromField } from './helpers.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const JAVA_VIS = new Set(['public', 'private', 'protected']);
|
||||
|
||||
export const javaConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Java,
|
||||
typeDeclarationNodes: [
|
||||
'class_declaration',
|
||||
'interface_declaration',
|
||||
'enum_declaration',
|
||||
'record_declaration',
|
||||
],
|
||||
fieldNodeTypes: ['field_declaration'],
|
||||
bodyNodeTypes: ['class_body', 'interface_body', 'enum_body'],
|
||||
defaultVisibility: 'package',
|
||||
|
||||
extractName(node) {
|
||||
// field_declaration > declarator:(variable_declarator name:(identifier))
|
||||
const declarator = node.childForFieldName('declarator');
|
||||
if (declarator) {
|
||||
const name = declarator.childForFieldName('name');
|
||||
return name?.text;
|
||||
}
|
||||
// fallback: walk children for variable_declarator
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'variable_declarator') {
|
||||
const name = child.childForFieldName('name');
|
||||
return name?.text;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// field_declaration > type:(type_identifier|generic_type|...)
|
||||
const t = typeFromField(node, 'type');
|
||||
if (t) return t;
|
||||
// fallback: first named child that looks like a type
|
||||
const first = node.firstNamedChild;
|
||||
if (first && first.type !== 'modifiers') {
|
||||
return extractSimpleTypeName(first) ?? first.text?.trim();
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
return findVisibility(node, JAVA_VIS, 'package', 'modifiers');
|
||||
},
|
||||
|
||||
isStatic(node) {
|
||||
return hasKeyword(node, 'static') || hasModifier(node, 'modifiers', 'static');
|
||||
},
|
||||
|
||||
isReadonly(node) {
|
||||
return hasKeyword(node, 'final') || hasModifier(node, 'modifiers', 'final');
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kotlin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const KOTLIN_VIS = new Set(['public', 'private', 'protected', 'internal']);
|
||||
|
||||
export const kotlinConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Kotlin,
|
||||
typeDeclarationNodes: [
|
||||
'class_declaration',
|
||||
'object_declaration',
|
||||
],
|
||||
fieldNodeTypes: ['property_declaration'],
|
||||
bodyNodeTypes: ['class_body'],
|
||||
defaultVisibility: 'public',
|
||||
|
||||
extractName(node) {
|
||||
// property_declaration > variable_declaration > simple_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 ident = child.namedChild(j);
|
||||
if (ident?.type === 'simple_identifier') return ident.text;
|
||||
}
|
||||
}
|
||||
if (child?.type === 'simple_identifier') return child.text;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// property_declaration may have a user_type or type_identifier under variable_declaration
|
||||
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 t = child.namedChild(j);
|
||||
if (t && (t.type === 'user_type' || t.type === 'type_identifier'
|
||||
|| t.type === 'nullable_type' || t.type === 'generic_type')) {
|
||||
return extractSimpleTypeName(t) ?? t.text?.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (child?.type === 'user_type' || child?.type === 'nullable_type') {
|
||||
return extractSimpleTypeName(child) ?? child.text?.trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
return findVisibility(node, KOTLIN_VIS, 'public', 'modifiers');
|
||||
},
|
||||
|
||||
isStatic(_node) {
|
||||
// Kotlin doesn't have static; companion object members are handled separately
|
||||
return false;
|
||||
},
|
||||
|
||||
isReadonly(node) {
|
||||
// 'val' = readonly, 'var' = mutable
|
||||
return hasKeyword(node, 'val');
|
||||
},
|
||||
};
|
||||
75
gitnexus/src/core/ingestion/field-extractors/configs/php.ts
Normal file
75
gitnexus/src/core/ingestion/field-extractors/configs/php.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/php.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { findVisibility, hasKeyword } from './helpers.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
|
||||
const PHP_VIS = new Set(['public', 'private', 'protected']);
|
||||
|
||||
/**
|
||||
* PHP field extraction config.
|
||||
*
|
||||
* Handles property_declaration inside class/interface/trait bodies.
|
||||
* tree-sitter-php uses 'declaration_list' for the class body.
|
||||
*/
|
||||
export const phpConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.PHP,
|
||||
typeDeclarationNodes: [
|
||||
'class_declaration',
|
||||
'interface_declaration',
|
||||
'trait_declaration',
|
||||
],
|
||||
fieldNodeTypes: ['property_declaration'],
|
||||
bodyNodeTypes: ['declaration_list'],
|
||||
defaultVisibility: 'public',
|
||||
|
||||
extractName(node) {
|
||||
// property_declaration > property_element > variable_name ($varName)
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'property_element') {
|
||||
const varName = child.childForFieldName('name')
|
||||
?? child.firstNamedChild;
|
||||
if (varName) {
|
||||
// strip leading $ from PHP variable names
|
||||
const text = varName.text;
|
||||
return text.startsWith('$') ? text.slice(1) : text;
|
||||
}
|
||||
}
|
||||
// fallback: variable_name direct child
|
||||
if (child?.type === 'variable_name') {
|
||||
const text = child.text;
|
||||
return text.startsWith('$') ? text.slice(1) : text;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// property_declaration may have a type before the property_element
|
||||
// tree-sitter-php: type can be union_type, named_type, optional_type, primitive_type
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'union_type' || child.type === 'named_type'
|
||||
|| child.type === 'optional_type' || child.type === 'primitive_type'
|
||||
|| child.type === 'intersection_type' || child.type === 'nullable_type') {
|
||||
return extractSimpleTypeName(child) ?? child.text?.trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
return findVisibility(node, PHP_VIS, 'public');
|
||||
},
|
||||
|
||||
isStatic(node) {
|
||||
return hasKeyword(node, 'static');
|
||||
},
|
||||
|
||||
isReadonly(node) {
|
||||
return hasKeyword(node, 'readonly');
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/python.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
|
||||
/**
|
||||
* Python field extraction config.
|
||||
*
|
||||
* Python class fields appear as:
|
||||
* - Annotated assignments: `name: str = ""`
|
||||
* - Plain assignments in __init__: `self.name = value`
|
||||
*
|
||||
* For AST-level extraction we handle expression_statement containing
|
||||
* assignment or type nodes inside a class body block.
|
||||
*/
|
||||
export const pythonConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Python,
|
||||
typeDeclarationNodes: ['class_definition'],
|
||||
fieldNodeTypes: ['expression_statement'],
|
||||
bodyNodeTypes: ['block'],
|
||||
defaultVisibility: 'public',
|
||||
|
||||
extractName(node) {
|
||||
// expression_statement wrapping an assignment or type
|
||||
const inner = node.firstNamedChild;
|
||||
if (!inner) return undefined;
|
||||
|
||||
// Annotated assignment: name: str = "default"
|
||||
// tree-sitter node: type (expression_statement (type (identifier) (type) ...))
|
||||
if (inner.type === 'type') {
|
||||
const ident = inner.childForFieldName('name') ?? inner.firstNamedChild;
|
||||
return ident?.type === 'identifier' ? ident.text : undefined;
|
||||
}
|
||||
|
||||
// assignment: x = 5 (class variable)
|
||||
if (inner.type === 'assignment') {
|
||||
const left = inner.childForFieldName('left');
|
||||
if (left?.type === 'identifier') return left.text;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
const inner = node.firstNamedChild;
|
||||
if (!inner) return undefined;
|
||||
|
||||
// Annotated assignment with value: `name: str = "default"`
|
||||
// AST: expression_statement > type > [identifier, type, ...]
|
||||
if (inner.type === 'type') {
|
||||
const typeNode = inner.childForFieldName('type') ?? inner.namedChild(1);
|
||||
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
}
|
||||
|
||||
// Annotation without value: `address: Address`
|
||||
// AST: expression_statement > assignment > [identifier, type]
|
||||
if (inner.type === 'assignment') {
|
||||
for (let i = 0; i < inner.childCount; i++) {
|
||||
const child = inner.child(i);
|
||||
if (child?.type === 'type') {
|
||||
const typeId = child.firstNamedChild;
|
||||
if (typeId) return extractSimpleTypeName(typeId) ?? typeId.text?.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
const inner = node.firstNamedChild;
|
||||
let name: string | undefined;
|
||||
if (inner?.type === 'type') {
|
||||
const ident = inner.childForFieldName('name') ?? inner.firstNamedChild;
|
||||
name = ident?.text;
|
||||
} else if (inner?.type === 'assignment') {
|
||||
const left = inner.childForFieldName('left');
|
||||
name = left?.text;
|
||||
}
|
||||
if (!name) return 'public';
|
||||
if (name.startsWith('__') && !name.endsWith('__')) return 'private';
|
||||
if (name.startsWith('_')) return 'protected';
|
||||
return 'public';
|
||||
},
|
||||
|
||||
isStatic(_node) {
|
||||
// Class-level variables in Python are effectively static;
|
||||
// instance variables (self.x) live in __init__ and are not extracted here.
|
||||
return false;
|
||||
},
|
||||
|
||||
isReadonly(_node) {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
65
gitnexus/src/core/ingestion/field-extractors/configs/ruby.ts
Normal file
65
gitnexus/src/core/ingestion/field-extractors/configs/ruby.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/ruby.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
|
||||
/**
|
||||
* Ruby field extraction config.
|
||||
*
|
||||
* Ruby is unusual: there are no field declarations in the traditional sense.
|
||||
* Fields are instance variables (@var) created by assignment, or declared
|
||||
* via attr_accessor / attr_reader / attr_writer calls.
|
||||
*
|
||||
* We detect:
|
||||
* - `call` nodes for attr_accessor / attr_reader / attr_writer
|
||||
* (their arguments are symbol names → field names)
|
||||
*
|
||||
* For simplicity we focus on attr_* calls in the class body.
|
||||
* Instance variable assignments (self.x = ...) would require deeper analysis.
|
||||
*/
|
||||
export const rubyConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Ruby,
|
||||
typeDeclarationNodes: ['class'],
|
||||
fieldNodeTypes: ['call'],
|
||||
bodyNodeTypes: ['body_statement'],
|
||||
defaultVisibility: 'public',
|
||||
|
||||
extractName(node) {
|
||||
// call node: method = attr_accessor / attr_reader / attr_writer
|
||||
const method = node.childForFieldName('method');
|
||||
if (!method) return undefined;
|
||||
const methodName = method.text;
|
||||
if (methodName !== 'attr_accessor' && methodName !== 'attr_reader'
|
||||
&& methodName !== 'attr_writer') {
|
||||
return undefined;
|
||||
}
|
||||
// arguments: argument_list > simple_symbol (:name)
|
||||
const args = node.childForFieldName('arguments');
|
||||
if (!args) return undefined;
|
||||
const firstArg = args.firstNamedChild;
|
||||
if (!firstArg) return undefined;
|
||||
// simple_symbol text is :name — strip the colon
|
||||
const text = firstArg.text;
|
||||
return text.startsWith(':') ? text.slice(1) : text;
|
||||
},
|
||||
|
||||
extractType(_node) {
|
||||
// Ruby is dynamically typed; no type annotations in standard Ruby
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
// attr_accessor/attr_writer fields are effectively public
|
||||
// attr_reader fields are read-only from outside but still public
|
||||
return 'public';
|
||||
},
|
||||
|
||||
isStatic(_node) {
|
||||
return false;
|
||||
},
|
||||
|
||||
isReadonly(node) {
|
||||
const method = node.childForFieldName('method');
|
||||
return method?.text === 'attr_reader';
|
||||
},
|
||||
};
|
||||
59
gitnexus/src/core/ingestion/field-extractors/configs/rust.ts
Normal file
59
gitnexus/src/core/ingestion/field-extractors/configs/rust.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/rust.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
import { hasKeyword } from './helpers.js';
|
||||
|
||||
/**
|
||||
* Rust field extraction config.
|
||||
*
|
||||
* Handles struct fields (named and tuple variants are out of scope).
|
||||
* Visibility: `pub` keyword = public, otherwise private (crate-private).
|
||||
* All fields are immutable by default in Rust (mutability is on the binding).
|
||||
*/
|
||||
export const rustConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Rust,
|
||||
typeDeclarationNodes: [
|
||||
'struct_item',
|
||||
'enum_item',
|
||||
],
|
||||
fieldNodeTypes: ['field_declaration'],
|
||||
bodyNodeTypes: ['field_declaration_list'],
|
||||
defaultVisibility: 'private',
|
||||
|
||||
extractName(node) {
|
||||
const name = node.childForFieldName('name');
|
||||
if (name) return name.text;
|
||||
// fallback: first field_identifier
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'field_identifier') return child.text;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
const typeNode = node.childForFieldName('type');
|
||||
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
// Check for visibility_modifier named child (pub, pub(crate), pub(super))
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'visibility_modifier') return 'public';
|
||||
}
|
||||
return hasKeyword(node, 'pub') ? 'public' : 'private';
|
||||
},
|
||||
|
||||
isStatic(_node) {
|
||||
return false; // Rust struct fields are never static
|
||||
},
|
||||
|
||||
isReadonly(_node) {
|
||||
// All Rust fields are immutable by default (mutability is per-binding)
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/swift.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { hasKeyword, findVisibility } from './helpers.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
|
||||
const SWIFT_VIS = new Set(['public', 'private', 'fileprivate', 'internal', 'open']);
|
||||
|
||||
/**
|
||||
* Swift field extraction config.
|
||||
*
|
||||
* Handles property_declaration inside class_body / protocol_body.
|
||||
* tree-sitter-swift uses property_declaration for stored/computed properties.
|
||||
*/
|
||||
export const swiftConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Swift,
|
||||
typeDeclarationNodes: [
|
||||
'class_declaration',
|
||||
'struct_declaration',
|
||||
'protocol_declaration',
|
||||
],
|
||||
fieldNodeTypes: ['property_declaration'],
|
||||
bodyNodeTypes: ['class_body', 'protocol_body'],
|
||||
defaultVisibility: 'internal',
|
||||
|
||||
extractName(node) {
|
||||
// property_declaration > pattern > simple_identifier
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'pattern') {
|
||||
for (let j = 0; j < child.namedChildCount; j++) {
|
||||
const ident = child.namedChild(j);
|
||||
if (ident?.type === 'simple_identifier') return ident.text;
|
||||
}
|
||||
return child.text;
|
||||
}
|
||||
if (child?.type === 'simple_identifier') return child.text;
|
||||
}
|
||||
// fallback: childForFieldName('name')
|
||||
const name = node.childForFieldName('name');
|
||||
return name?.text;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// property_declaration > type_annotation > type_identifier
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'type_annotation') {
|
||||
const inner = child.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner) ?? inner.text?.trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
return findVisibility(node, SWIFT_VIS, 'internal', 'modifiers');
|
||||
},
|
||||
|
||||
isStatic(node) {
|
||||
return hasKeyword(node, 'static') || hasKeyword(node, 'class');
|
||||
},
|
||||
|
||||
isReadonly(node) {
|
||||
// 'let' = constant/readonly, 'var' = variable
|
||||
return hasKeyword(node, 'let');
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/typescript-javascript.ts
|
||||
|
||||
import { SupportedLanguages } from '../../../../config/supported-languages.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { hasKeyword, findVisibility, typeFromAnnotation } from './helpers.js';
|
||||
|
||||
const VISIBILITY_KEYWORDS = new Set(['public', 'private', 'protected']);
|
||||
|
||||
const shared: Omit<FieldExtractionConfig, 'language'> = {
|
||||
typeDeclarationNodes: [
|
||||
'class_declaration',
|
||||
'abstract_class_declaration',
|
||||
'interface_declaration',
|
||||
],
|
||||
fieldNodeTypes: [
|
||||
'public_field_definition',
|
||||
'property_signature',
|
||||
'field_definition',
|
||||
],
|
||||
bodyNodeTypes: ['class_body', 'interface_body', 'object_type'],
|
||||
defaultVisibility: 'public',
|
||||
|
||||
extractName(node) {
|
||||
const nameNode = node.childForFieldName('name') ?? node.childForFieldName('property');
|
||||
return nameNode?.text;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
// tree-sitter TS uses a named 'type' field for type_annotation
|
||||
const typeField = node.childForFieldName('type');
|
||||
if (typeField) {
|
||||
if (typeField.type === 'type_annotation') {
|
||||
const inner = typeField.firstNamedChild;
|
||||
return inner?.text?.trim();
|
||||
}
|
||||
return typeField.text?.trim();
|
||||
}
|
||||
return typeFromAnnotation(node);
|
||||
},
|
||||
|
||||
extractVisibility(node) {
|
||||
// TypeScript accessibility_modifier
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'accessibility_modifier') {
|
||||
const t = child.text.trim();
|
||||
if (VISIBILITY_KEYWORDS.has(t)) return t;
|
||||
}
|
||||
}
|
||||
return findVisibility(node, VISIBILITY_KEYWORDS, 'public', 'modifiers');
|
||||
},
|
||||
|
||||
isStatic(node) {
|
||||
return hasKeyword(node, 'static');
|
||||
},
|
||||
|
||||
isReadonly(node) {
|
||||
return hasKeyword(node, 'readonly');
|
||||
},
|
||||
};
|
||||
|
||||
export const typescriptConfig: FieldExtractionConfig = {
|
||||
...shared,
|
||||
language: SupportedLanguages.TypeScript,
|
||||
};
|
||||
|
||||
export const javascriptConfig: FieldExtractionConfig = {
|
||||
...shared,
|
||||
language: SupportedLanguages.JavaScript,
|
||||
};
|
||||
153
gitnexus/src/core/ingestion/field-extractors/generic.ts
Normal file
153
gitnexus/src/core/ingestion/field-extractors/generic.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/generic.ts
|
||||
|
||||
/**
|
||||
* Generic table-driven field extractor factory.
|
||||
*
|
||||
* Instead of 14 separate 300-line files, define a config per language and
|
||||
* generate extractors from configs. The factory creates a class extending
|
||||
* BaseFieldExtractor whose behaviour is entirely driven by FieldExtractionConfig.
|
||||
*/
|
||||
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
||||
import { BaseFieldExtractor } from '../field-extractor.js';
|
||||
import type { FieldExtractor } from '../field-extractor.js';
|
||||
import type { FieldExtractorContext, ExtractedFields, FieldInfo } from '../field-types.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface FieldExtractionConfig {
|
||||
language: SupportedLanguages;
|
||||
/** AST node types that are class/struct/interface declarations */
|
||||
typeDeclarationNodes: string[];
|
||||
/** AST node types that represent field/property declarations inside a body */
|
||||
fieldNodeTypes: string[];
|
||||
/** AST node type(s) for the class body container (e.g., 'class_body', 'declaration_list') */
|
||||
bodyNodeTypes: string[];
|
||||
/** Default visibility when no modifier is present */
|
||||
defaultVisibility: string;
|
||||
/** Extract field name from a field declaration node */
|
||||
extractName: (node: SyntaxNode) => string | undefined;
|
||||
/** Extract type annotation from a field declaration node */
|
||||
extractType: (node: SyntaxNode) => string | undefined;
|
||||
/** Extract visibility from a field declaration node */
|
||||
extractVisibility: (node: SyntaxNode) => string;
|
||||
/** Check if a field is static */
|
||||
isStatic: (node: SyntaxNode) => boolean;
|
||||
/** Check if a field is readonly/final/const */
|
||||
isReadonly: (node: SyntaxNode) => boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a FieldExtractor from a declarative config.
|
||||
*/
|
||||
export function createFieldExtractor(config: FieldExtractionConfig): FieldExtractor {
|
||||
const typeDeclarationSet = new Set(config.typeDeclarationNodes);
|
||||
const fieldNodeSet = new Set(config.fieldNodeTypes);
|
||||
const bodyNodeSet = new Set(config.bodyNodeTypes);
|
||||
|
||||
class GenericFieldExtractor extends BaseFieldExtractor {
|
||||
language = config.language;
|
||||
|
||||
isTypeDeclaration(node: SyntaxNode): boolean {
|
||||
return typeDeclarationSet.has(node.type);
|
||||
}
|
||||
|
||||
protected extractVisibility(node: SyntaxNode): string {
|
||||
return config.extractVisibility(node);
|
||||
}
|
||||
|
||||
extract(node: SyntaxNode, context: FieldExtractorContext): ExtractedFields | null {
|
||||
if (!this.isTypeDeclaration(node)) return null;
|
||||
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return null;
|
||||
|
||||
const ownerFqn = nameNode.text;
|
||||
const fields: FieldInfo[] = [];
|
||||
|
||||
// Find body container(s)
|
||||
const bodies = this.findBodies(node);
|
||||
for (const body of bodies) {
|
||||
this.extractFieldsFromBody(body, context, fields);
|
||||
}
|
||||
|
||||
return { ownerFqn, fields, nestedTypes: [] };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// private helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private findBodies(node: SyntaxNode): SyntaxNode[] {
|
||||
const result: SyntaxNode[] = [];
|
||||
// Try named 'body' field first
|
||||
const bodyField = node.childForFieldName('body');
|
||||
if (bodyField && bodyNodeSet.has(bodyField.type)) {
|
||||
result.push(bodyField);
|
||||
return result;
|
||||
}
|
||||
// Walk immediate children for matching body node types
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && bodyNodeSet.has(child.type)) {
|
||||
result.push(child);
|
||||
}
|
||||
}
|
||||
// Fallback: use the body field even if its type is not in bodyNodeSet
|
||||
if (result.length === 0 && bodyField) {
|
||||
result.push(bodyField);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private extractFieldsFromBody(
|
||||
body: SyntaxNode,
|
||||
context: FieldExtractorContext,
|
||||
out: FieldInfo[],
|
||||
): void {
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const child = body.namedChild(i);
|
||||
if (!child) continue;
|
||||
|
||||
if (fieldNodeSet.has(child.type)) {
|
||||
const field = this.extractSingleField(child, context);
|
||||
if (field) out.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractSingleField(
|
||||
node: SyntaxNode,
|
||||
context: FieldExtractorContext,
|
||||
): FieldInfo | null {
|
||||
const name = config.extractName(node);
|
||||
if (!name) return null;
|
||||
|
||||
let type: string | null = config.extractType(node) ?? null;
|
||||
if (type) {
|
||||
type = this.normalizeType(type);
|
||||
const resolved = this.resolveType(type, context);
|
||||
if (resolved) type = resolved;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
visibility: config.extractVisibility(node),
|
||||
isStatic: config.isStatic(node),
|
||||
isReadonly: config.isReadonly(node),
|
||||
sourceFile: context.filePath,
|
||||
line: node.startPosition.row + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new GenericFieldExtractor();
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/index.ts
|
||||
|
||||
/**
|
||||
* Field Extractors Index
|
||||
*
|
||||
* Language-specific field extractors for extracting field/property definitions
|
||||
* from class/struct/interface declarations across supported languages.
|
||||
*
|
||||
* Each extractor:
|
||||
* 1. Extends BaseFieldExtractor
|
||||
* 2. Implements isTypeDeclaration() to recognize type declarations
|
||||
* 3. Implements extract() to pull field definitions with:
|
||||
* - Name and type
|
||||
* - Visibility modifiers
|
||||
* - Static/readonly flags
|
||||
*/
|
||||
|
||||
import type { FieldExtractor } from '../field-extractor.js';
|
||||
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
||||
import { typescriptFieldExtractor, TypeScriptFieldExtractor } from './typescript.js';
|
||||
|
||||
// Re-export for direct usage
|
||||
export { typescriptFieldExtractor, TypeScriptFieldExtractor };
|
||||
|
||||
/**
|
||||
* Registry of all available field extractors.
|
||||
* Populated lazily on first access.
|
||||
*/
|
||||
let extractorRegistry: Map<SupportedLanguages, FieldExtractor> | null = null;
|
||||
|
||||
/**
|
||||
* Get all available field extractors.
|
||||
* Returns a map from language to extractor instance.
|
||||
*/
|
||||
export function getFieldExtractors(): Map<SupportedLanguages, FieldExtractor> {
|
||||
if (!extractorRegistry) {
|
||||
extractorRegistry = new Map();
|
||||
|
||||
// Register TypeScript extractor
|
||||
extractorRegistry.set(SupportedLanguages.TypeScript, typescriptFieldExtractor);
|
||||
|
||||
// Future: Add other language extractors here
|
||||
// extractorRegistry.set(SupportedLanguages.Java, javaFieldExtractor);
|
||||
// extractorRegistry.set(SupportedLanguages.CSharp, csharpFieldExtractor);
|
||||
// extractorRegistry.set(SupportedLanguages.Python, pythonFieldExtractor);
|
||||
// etc.
|
||||
}
|
||||
|
||||
return extractorRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a field extractor for a specific language.
|
||||
* Returns undefined if no extractor is available for that language.
|
||||
*/
|
||||
export function getFieldExtractor(language: SupportedLanguages): FieldExtractor | undefined {
|
||||
return getFieldExtractors().get(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field extractor is available for a given language.
|
||||
*/
|
||||
export function hasFieldExtractor(language: SupportedLanguages): boolean {
|
||||
return getFieldExtractors().has(language);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import type { SupportedLanguages } from '../../config/supported-languages.js';
|
|||
import type { LanguageTypeConfig } from './type-extractors/types.js';
|
||||
import type { CallRouter } from './call-routing.js';
|
||||
import type { ExportChecker } from './export-detection.js';
|
||||
import type { FieldExtractor } from './field-extractor.js';
|
||||
import type { ImportResolverFn } from './import-resolvers/types.js';
|
||||
import type { NamedBindingExtractorFn } from './named-bindings/types.js';
|
||||
import type { SyntaxNode } from './utils/ast-helpers.js';
|
||||
|
|
@ -98,6 +99,10 @@ interface LanguageProviderConfig {
|
|||
readonly mroStrategy?: MroStrategy;
|
||||
|
||||
// ── Language-specific extraction hooks ────────────────────────────
|
||||
/** Field extractor for extracting field/property definitions from class/struct
|
||||
* declarations. Produces FieldInfo[] with name, type, visibility, static,
|
||||
* readonly metadata. Default: undefined (no field extraction). */
|
||||
readonly fieldExtractor?: FieldExtractor;
|
||||
/** Extract a semantic description for a definition node (e.g., PHP Eloquent
|
||||
* property arrays, relation method descriptions).
|
||||
* Default: undefined (no description extraction). */
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { C_QUERIES, CPP_QUERIES } from '../tree-sitter-queries.js';
|
|||
|
||||
import { isCppInsideClassOrStruct } from '../utils/ast-helpers.js';
|
||||
import type { LanguageProvider } from '../language-provider.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { cConfig as cFieldConfig, cppConfig as cppFieldConfig } from '../field-extractors/configs/c-cpp.js';
|
||||
|
||||
/** Label override shared by C and C++: skip function_definition captures inside class/struct
|
||||
* bodies (they're duplicates of definition.method captures). */
|
||||
|
|
@ -33,6 +35,7 @@ export const cProvider = defineLanguage({
|
|||
exportChecker: cCppExportChecker,
|
||||
importResolver: resolveCImport,
|
||||
importSemantics: 'wildcard',
|
||||
fieldExtractor: createFieldExtractor(cFieldConfig),
|
||||
labelOverride: cppLabelOverride,
|
||||
});
|
||||
|
||||
|
|
@ -45,5 +48,6 @@ export const cppProvider = defineLanguage({
|
|||
importResolver: resolveCppImport,
|
||||
importSemantics: 'wildcard',
|
||||
mroStrategy: 'leftmost-base',
|
||||
fieldExtractor: createFieldExtractor(cppFieldConfig),
|
||||
labelOverride: cppLabelOverride,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { csharpExportChecker } from '../export-detection.js';
|
|||
import { resolveCSharpImport } from '../import-resolvers/csharp.js';
|
||||
import { extractCSharpNamedBindings } from '../named-bindings/csharp.js';
|
||||
import { CSHARP_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { csharpConfig as csharpFieldConfig } from '../field-extractors/configs/csharp.js';
|
||||
|
||||
export const csharpProvider = defineLanguage({
|
||||
id: SupportedLanguages.CSharp,
|
||||
|
|
@ -24,4 +26,5 @@ export const csharpProvider = defineLanguage({
|
|||
namedBindingExtractor: extractCSharpNamedBindings,
|
||||
interfaceNamePattern: /^I[A-Z]/,
|
||||
mroStrategy: 'implements-split',
|
||||
fieldExtractor: createFieldExtractor(csharpFieldConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { typeConfig as dartConfig } from '../type-extractors/dart.js';
|
|||
import { dartExportChecker } from '../export-detection.js';
|
||||
import { resolveDartImport } from '../import-resolvers/dart.js';
|
||||
import { DART_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { dartConfig as dartFieldConfig } from '../field-extractors/configs/dart.js';
|
||||
|
||||
export const dartProvider = defineLanguage({
|
||||
id: SupportedLanguages.Dart,
|
||||
|
|
@ -22,4 +24,5 @@ export const dartProvider = defineLanguage({
|
|||
exportChecker: dartExportChecker,
|
||||
importResolver: resolveDartImport,
|
||||
importSemantics: 'wildcard',
|
||||
fieldExtractor: createFieldExtractor(dartFieldConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { typeConfig as goConfig } from '../type-extractors/go.js';
|
|||
import { goExportChecker } from '../export-detection.js';
|
||||
import { resolveGoImport } from '../import-resolvers/go.js';
|
||||
import { GO_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { goConfig as goFieldConfig } from '../field-extractors/configs/go.js';
|
||||
|
||||
export const goProvider = defineLanguage({
|
||||
id: SupportedLanguages.Go,
|
||||
|
|
@ -24,4 +26,5 @@ export const goProvider = defineLanguage({
|
|||
exportChecker: goExportChecker,
|
||||
importResolver: resolveGoImport,
|
||||
importSemantics: 'wildcard',
|
||||
fieldExtractor: createFieldExtractor(goFieldConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import { javaExportChecker } from '../export-detection.js';
|
|||
import { resolveJavaImport } from '../import-resolvers/jvm.js';
|
||||
import { extractJavaNamedBindings } from '../named-bindings/java.js';
|
||||
import { JAVA_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { javaConfig } from '../field-extractors/configs/jvm.js';
|
||||
|
||||
export const javaProvider = defineLanguage({
|
||||
id: SupportedLanguages.Java,
|
||||
|
|
@ -25,4 +27,5 @@ export const javaProvider = defineLanguage({
|
|||
namedBindingExtractor: extractJavaNamedBindings,
|
||||
interfaceNamePattern: /^I[A-Z]/,
|
||||
mroStrategy: 'implements-split',
|
||||
fieldExtractor: createFieldExtractor(javaConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { extractKotlinNamedBindings } from '../named-bindings/kotlin.js';
|
|||
import { appendKotlinWildcard } from '../import-resolvers/jvm.js';
|
||||
import { KOTLIN_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { isKotlinClassMethod } from '../utils/ast-helpers.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { kotlinConfig } from '../field-extractors/configs/jvm.js';
|
||||
|
||||
export const kotlinProvider = defineLanguage({
|
||||
id: SupportedLanguages.Kotlin,
|
||||
|
|
@ -27,6 +29,7 @@ export const kotlinProvider = defineLanguage({
|
|||
namedBindingExtractor: extractKotlinNamedBindings,
|
||||
importPathPreprocessor: appendKotlinWildcard,
|
||||
mroStrategy: 'implements-split',
|
||||
fieldExtractor: createFieldExtractor(kotlinConfig),
|
||||
labelOverride: (functionNode, defaultLabel) => {
|
||||
if (defaultLabel !== 'Function') return defaultLabel;
|
||||
if (isKotlinClassMethod(functionNode)) return 'Method';
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { extractPhpNamedBindings } from '../named-bindings/php.js';
|
|||
import { PHP_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { findDescendant, extractStringContent } from '../utils/ast-helpers.js';
|
||||
import type { NodeLabel } from '../../graph/types.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { phpConfig as phpFieldConfig } from '../field-extractors/configs/php.js';
|
||||
|
||||
/** Eloquent model properties whose array values are worth indexing. */
|
||||
const ELOQUENT_ARRAY_PROPS = new Set(['fillable', 'casts', 'hidden', 'guarded', 'with', 'appends']);
|
||||
|
|
@ -128,6 +130,7 @@ export const phpProvider = defineLanguage({
|
|||
exportChecker: phpExportChecker,
|
||||
importResolver: resolvePhpImport,
|
||||
namedBindingExtractor: extractPhpNamedBindings,
|
||||
fieldExtractor: createFieldExtractor(phpFieldConfig),
|
||||
descriptionExtractor: phpDescriptionExtractor,
|
||||
isRouteFile: isPhpRouteFile,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { pythonExportChecker } from '../export-detection.js';
|
|||
import { resolvePythonImport } from '../import-resolvers/python.js';
|
||||
import { extractPythonNamedBindings } from '../named-bindings/python.js';
|
||||
import { PYTHON_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { pythonConfig as pythonFieldConfig } from '../field-extractors/configs/python.js';
|
||||
|
||||
export const pythonProvider = defineLanguage({
|
||||
id: SupportedLanguages.Python,
|
||||
|
|
@ -28,4 +30,5 @@ export const pythonProvider = defineLanguage({
|
|||
namedBindingExtractor: extractPythonNamedBindings,
|
||||
importSemantics: 'namespace',
|
||||
mroStrategy: 'c3',
|
||||
fieldExtractor: createFieldExtractor(pythonFieldConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import { routeRubyCall } from '../call-routing.js';
|
|||
import { rubyExportChecker } from '../export-detection.js';
|
||||
import { resolveRubyImport } from '../import-resolvers/ruby.js';
|
||||
import { RUBY_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { rubyConfig as rubyFieldConfig } from '../field-extractors/configs/ruby.js';
|
||||
|
||||
export const rubyProvider = defineLanguage({
|
||||
id: SupportedLanguages.Ruby,
|
||||
|
|
@ -24,4 +26,5 @@ export const rubyProvider = defineLanguage({
|
|||
importResolver: resolveRubyImport,
|
||||
callRouter: routeRubyCall,
|
||||
importSemantics: 'wildcard',
|
||||
fieldExtractor: createFieldExtractor(rubyFieldConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { rustExportChecker } from '../export-detection.js';
|
|||
import { resolveRustImport } from '../import-resolvers/rust.js';
|
||||
import { extractRustNamedBindings } from '../named-bindings/rust.js';
|
||||
import { RUST_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { rustConfig as rustFieldConfig } from '../field-extractors/configs/rust.js';
|
||||
|
||||
export const rustProvider = defineLanguage({
|
||||
id: SupportedLanguages.Rust,
|
||||
|
|
@ -27,4 +29,5 @@ export const rustProvider = defineLanguage({
|
|||
importResolver: resolveRustImport,
|
||||
namedBindingExtractor: extractRustNamedBindings,
|
||||
mroStrategy: 'qualified-syntax',
|
||||
fieldExtractor: createFieldExtractor(rustFieldConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { swiftExportChecker } from '../export-detection.js';
|
|||
import { resolveSwiftImport } from '../import-resolvers/swift.js';
|
||||
import { SWIFT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import type { SwiftPackageConfig } from '../language-config.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { swiftConfig as swiftFieldConfig } from '../field-extractors/configs/swift.js';
|
||||
|
||||
/**
|
||||
* Group Swift files by SPM target for implicit module visibility.
|
||||
|
|
@ -110,5 +112,6 @@ export const swiftProvider = defineLanguage({
|
|||
importResolver: resolveSwiftImport,
|
||||
importSemantics: 'wildcard',
|
||||
heritageDefaultEdge: 'IMPLEMENTS',
|
||||
fieldExtractor: createFieldExtractor(swiftFieldConfig),
|
||||
implicitImportWirer: wireSwiftImplicitImports,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import { tsExportChecker } from '../export-detection.js';
|
|||
import { resolveTypescriptImport, resolveJavascriptImport } from '../import-resolvers/standard.js';
|
||||
import { extractTsNamedBindings } from '../named-bindings/typescript.js';
|
||||
import { TYPESCRIPT_QUERIES, JAVASCRIPT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { typescriptFieldExtractor } from '../field-extractors/typescript.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { javascriptConfig } from '../field-extractors/configs/typescript-javascript.js';
|
||||
|
||||
export const typescriptProvider = defineLanguage({
|
||||
id: SupportedLanguages.TypeScript,
|
||||
|
|
@ -23,6 +26,7 @@ export const typescriptProvider = defineLanguage({
|
|||
exportChecker: tsExportChecker,
|
||||
importResolver: resolveTypescriptImport,
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
fieldExtractor: typescriptFieldExtractor,
|
||||
});
|
||||
|
||||
export const javascriptProvider = defineLanguage({
|
||||
|
|
@ -33,4 +37,5 @@ export const javascriptProvider = defineLanguage({
|
|||
exportChecker: tsExportChecker,
|
||||
importResolver: resolveJavascriptImport,
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
fieldExtractor: createFieldExtractor(javascriptConfig),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import { SymbolTable } from './symbol-table.js';
|
|||
import { ASTCache } from './ast-cache.js';
|
||||
import { getLanguageFromFilename } from './utils/language-detection.js';
|
||||
import { yieldToEventLoop } from './utils/event-loop.js';
|
||||
import { getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, getLabelFromCaptures } from './utils/ast-helpers.js';
|
||||
import { extractPropertyDeclaredType } from './type-extractors/shared.js';
|
||||
import { getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, getLabelFromCaptures, CLASS_CONTAINER_TYPES } from './utils/ast-helpers.js';
|
||||
import { detectFrameworkFromAST } from './framework-detection.js';
|
||||
import { buildTypeEnv } from './type-env.js';
|
||||
import type { FieldInfo, FieldExtractorContext } from './field-types.js';
|
||||
import type { LanguageProvider } from './language-provider.js';
|
||||
import { WorkerPool } from './workers/worker-pool.js';
|
||||
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings, ExtractedORMQuery } from './workers/parse-worker.js';
|
||||
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
|
||||
|
|
@ -154,6 +156,43 @@ const cachedExportCheck = (checker: (node: any, name: string) => boolean, node:
|
|||
return result;
|
||||
};
|
||||
|
||||
// FieldExtractor cache for sequential path — same pattern as parse-worker.ts
|
||||
const seqFieldInfoCache = new Map<number, Map<string, FieldInfo>>();
|
||||
|
||||
function seqFindEnclosingClassNode(node: any): any | null {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (CLASS_CONTAINER_TYPES.has(current.type)) return current;
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Minimal no-op SymbolTable stub for FieldExtractorContext (sequential path has a real
|
||||
* SymbolTable, but it's incomplete at this stage — use the stub for safety). */
|
||||
const NOOP_SYMBOL_TABLE_SEQ: any = {
|
||||
lookupExactAll: () => [],
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
};
|
||||
|
||||
function seqGetFieldInfo(
|
||||
classNode: any,
|
||||
provider: LanguageProvider,
|
||||
context: FieldExtractorContext,
|
||||
): Map<string, FieldInfo> | undefined {
|
||||
if (!provider.fieldExtractor) return undefined;
|
||||
const cacheKey = classNode.startIndex as number;
|
||||
let cached = seqFieldInfoCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
const extracted = provider.fieldExtractor.extract(classNode, context);
|
||||
if (!extracted?.fields?.length) return undefined;
|
||||
cached = new Map<string, FieldInfo>();
|
||||
for (const field of extracted.fields) cached.set(field.name, field);
|
||||
seqFieldInfoCache.set(cacheKey, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
const processParsingSequential = async (
|
||||
graph: KnowledgeGraph,
|
||||
files: { path: string; content: string }[],
|
||||
|
|
@ -171,6 +210,7 @@ const processParsingSequential = async (
|
|||
// Reset memoization before each new file (node refs are per-tree)
|
||||
classIdCache.clear();
|
||||
exportCache.clear();
|
||||
seqFieldInfoCache.clear();
|
||||
|
||||
onFileProgress?.(i + 1, total, file.path);
|
||||
|
||||
|
|
@ -222,6 +262,9 @@ const processParsingSequential = async (
|
|||
continue;
|
||||
}
|
||||
|
||||
// Build per-file type environment for FieldExtractor context (lightweight — skipped if no fieldExtractor)
|
||||
const typeEnv = provider.fieldExtractor ? buildTypeEnv(tree, language) : null;
|
||||
|
||||
matches.forEach(match => {
|
||||
const captureMap: Record<string, any> = {};
|
||||
|
||||
|
|
@ -291,10 +334,36 @@ const processParsingSequential = async (
|
|||
const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
|
||||
const enclosingClassId = needsOwner ? cachedFindEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
|
||||
|
||||
// Extract declared type for Property nodes (field/property type annotations)
|
||||
const declaredType = (nodeLabel === 'Property' && definitionNode)
|
||||
? extractPropertyDeclaredType(definitionNode)
|
||||
: undefined;
|
||||
// Extract declared type and field metadata for Property nodes
|
||||
let declaredType: string | undefined;
|
||||
let seqVisibility: string | undefined;
|
||||
let seqIsStatic: boolean | undefined;
|
||||
let seqIsReadonly: boolean | undefined;
|
||||
if (nodeLabel === 'Property' && definitionNode) {
|
||||
// FieldExtractor is the single source of truth when available
|
||||
if (provider.fieldExtractor && typeEnv) {
|
||||
const classNode = seqFindEnclosingClassNode(definitionNode);
|
||||
if (classNode) {
|
||||
const fieldMap = seqGetFieldInfo(classNode, provider, {
|
||||
typeEnv, symbolTable: NOOP_SYMBOL_TABLE_SEQ, filePath: file.path, language,
|
||||
});
|
||||
const info = fieldMap?.get(nodeName);
|
||||
if (info) {
|
||||
declaredType = info.type ?? undefined;
|
||||
seqVisibility = info.visibility;
|
||||
seqIsStatic = info.isStatic;
|
||||
seqIsReadonly = info.isReadonly;
|
||||
}
|
||||
}
|
||||
}
|
||||
// All 14 languages register a FieldExtractor — no fallback needed.
|
||||
}
|
||||
|
||||
// Apply field metadata to the graph node retroactively
|
||||
if (seqVisibility !== undefined) node.properties.visibility = seqVisibility;
|
||||
if (seqIsStatic !== undefined) node.properties.isStatic = seqIsStatic;
|
||||
if (seqIsReadonly !== undefined) node.properties.isReadonly = seqIsReadonly;
|
||||
if (declaredType !== undefined) node.properties.declaredType = declaredType;
|
||||
|
||||
symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
|
||||
parameterCount: methodSig?.parameterCount,
|
||||
|
|
|
|||
|
|
@ -752,101 +752,6 @@ export const extractReturnTypeName = (raw: string, depth = 0): string | undefine
|
|||
return text;
|
||||
};
|
||||
|
||||
// ── Property declared-type extraction ────────────────────────────────────
|
||||
// Shared between parse-worker (worker path) and parsing-processor (sequential path).
|
||||
|
||||
/**
|
||||
* Extract the declared type of a property/field from its AST definition node.
|
||||
* Handles cross-language patterns:
|
||||
* - TypeScript: `name: Type` → type_annotation child
|
||||
* - Java: `Type name` → type child on field_declaration
|
||||
* - C#: `Type Name { get; set; }` → type child on property_declaration
|
||||
* - Go: `Name Type` → type child on field_declaration
|
||||
* - Kotlin: `var name: Type` → variable_declaration child with type field
|
||||
*
|
||||
* Returns the normalized type name, or undefined if no type can be extracted.
|
||||
*/
|
||||
export const extractPropertyDeclaredType = (definitionNode: SyntaxNode | null): string | undefined => {
|
||||
if (!definitionNode) return undefined;
|
||||
|
||||
// Strategy 1: Look for a `type` or `type_annotation` named field
|
||||
const typeNode = definitionNode.childForFieldName?.('type');
|
||||
if (typeNode) {
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName) return typeName;
|
||||
// Fallback: use the raw text (for complex types like User[] or List<User>)
|
||||
const text = typeNode.text?.trim();
|
||||
if (text && text.length < 100) return text;
|
||||
}
|
||||
|
||||
// Strategy 2: Walk children looking for type_annotation (TypeScript pattern)
|
||||
for (let i = 0; i < definitionNode.childCount; i++) {
|
||||
const child = definitionNode.child(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'type_annotation') {
|
||||
// Type annotation has the actual type as a child
|
||||
for (let j = 0; j < child.childCount; j++) {
|
||||
const typeChild = child.child(j);
|
||||
if (typeChild && typeChild.type !== ':') {
|
||||
const typeName = extractSimpleTypeName(typeChild);
|
||||
if (typeName) return typeName;
|
||||
const text = typeChild.text?.trim();
|
||||
if (text && text.length < 100) return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: For Java field_declaration, the type is a sibling of variable_declarator
|
||||
// AST: (field_declaration type: (type_identifier) declarator: (variable_declarator ...))
|
||||
const parentDecl = definitionNode.parent;
|
||||
if (parentDecl) {
|
||||
const parentType = parentDecl.childForFieldName?.('type');
|
||||
if (parentType) {
|
||||
const typeName = extractSimpleTypeName(parentType);
|
||||
if (typeName) return typeName;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: Kotlin property_declaration — type is nested inside variable_declaration child
|
||||
// AST: (property_declaration (variable_declaration (simple_identifier) ":" (user_type (type_identifier))))
|
||||
// Kotlin's variable_declaration has NO named 'type' field — children are all positional.
|
||||
for (let i = 0; i < definitionNode.childCount; i++) {
|
||||
const child = definitionNode.child(i);
|
||||
if (child?.type === 'variable_declaration') {
|
||||
// Try named field first (works for other languages sharing this strategy)
|
||||
const varType = child.childForFieldName?.('type');
|
||||
if (varType) {
|
||||
const typeName = extractSimpleTypeName(varType);
|
||||
if (typeName) return typeName;
|
||||
const text = varType.text?.trim();
|
||||
if (text && text.length < 100) return text;
|
||||
}
|
||||
// Fallback: walk unnamed children for user_type / type_identifier (Kotlin)
|
||||
for (let j = 0; j < child.namedChildCount; j++) {
|
||||
const varChild = child.namedChild(j);
|
||||
if (varChild && (varChild.type === 'user_type' || varChild.type === 'type_identifier'
|
||||
|| varChild.type === 'nullable_type' || varChild.type === 'generic_type')) {
|
||||
const typeName = extractSimpleTypeName(varChild);
|
||||
if (typeName) return typeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 5: PHP @var PHPDoc — look for preceding comment with @var Type
|
||||
// Handles pre-PHP-7.4 code: /** @var Address */ public $address;
|
||||
const prevSibling = definitionNode.previousNamedSibling ?? definitionNode.parent?.previousNamedSibling;
|
||||
if (prevSibling?.type === 'comment') {
|
||||
const commentText = prevSibling.text;
|
||||
const varMatch = commentText?.match(/@var\s+([A-Z][\w\\]*)/);
|
||||
if (varMatch) {
|
||||
// Strip namespace prefix: \App\Models\User → User
|
||||
const raw = varMatch[1];
|
||||
const base = raw.includes('\\') ? raw.split('\\').pop()! : raw;
|
||||
if (base && /^[A-Z]\w*$/.test(base)) return base;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
// extractPropertyDeclaredType removed — all 14 languages register a FieldExtractor
|
||||
// via defineLanguage() which is the single source of truth for Property metadata
|
||||
// (declaredType, visibility, isStatic, isReadonly).
|
||||
|
|
|
|||
|
|
@ -54,8 +54,9 @@ import { detectFrameworkFromAST } from '../framework-detection.js';
|
|||
import { generateId } from '../../../lib/utils.js';
|
||||
import { preprocessImportPath } from '../import-processor.js';
|
||||
import type { NamedBinding } from '../named-bindings/types.js';
|
||||
import { extractPropertyDeclaredType } from '../type-extractors/shared.js';
|
||||
import type { NodeLabel } from '../../graph/types.js';
|
||||
import type { FieldInfo, FieldExtractorContext } from '../field-types.js';
|
||||
import { CLASS_CONTAINER_TYPES } from '../utils/ast-helpers.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types for serializable results
|
||||
|
|
@ -77,6 +78,11 @@ interface ParsedNode {
|
|||
parameterCount?: number;
|
||||
requiredParameterCount?: number;
|
||||
returnType?: string;
|
||||
// Field/property metadata (populated by FieldExtractor)
|
||||
declaredType?: string;
|
||||
visibility?: string;
|
||||
isStatic?: boolean;
|
||||
isReadonly?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +106,9 @@ interface ParsedSymbol {
|
|||
returnType?: string;
|
||||
declaredType?: string;
|
||||
ownerId?: string;
|
||||
visibility?: string;
|
||||
isStatic?: boolean;
|
||||
isReadonly?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtractedImport {
|
||||
|
|
@ -288,7 +297,67 @@ const classIdCache = new Map<any, string | null>();
|
|||
const functionIdCache = new Map<any, string | null>();
|
||||
const exportCache = new Map<any, boolean>();
|
||||
|
||||
const clearCaches = (): void => { classIdCache.clear(); functionIdCache.clear(); exportCache.clear(); };
|
||||
const clearCaches = (): void => { classIdCache.clear(); functionIdCache.clear(); exportCache.clear(); fieldInfoCache.clear(); };
|
||||
|
||||
// ============================================================================
|
||||
// FieldExtractor cache — extract field metadata once per class, reuse for each property.
|
||||
// Keyed by class node startIndex (unique per AST node within a file).
|
||||
// ============================================================================
|
||||
|
||||
const fieldInfoCache = new Map<number, Map<string, FieldInfo>>();
|
||||
|
||||
/**
|
||||
* Walk up from a definition node to find the nearest enclosing class/struct/interface
|
||||
* AST node. Returns the SyntaxNode itself (not an ID) for passing to FieldExtractor.
|
||||
*/
|
||||
function findEnclosingClassNode(node: any): any | null {
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal no-op SymbolTable stub for FieldExtractorContext in the worker.
|
||||
* Field extraction only uses symbolTable.lookupExactAll for optional type resolution —
|
||||
* returning [] causes the extractor to use the raw type string, which is fine for us.
|
||||
*/
|
||||
const NOOP_SYMBOL_TABLE: any = {
|
||||
lookupExactAll: () => [],
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get (or extract and cache) field info for a class node.
|
||||
* Returns a name→FieldInfo map, or undefined if the provider has no field extractor
|
||||
* or the class yielded no fields.
|
||||
*/
|
||||
function getFieldInfo(
|
||||
classNode: any,
|
||||
provider: LanguageProvider,
|
||||
context: FieldExtractorContext,
|
||||
): Map<string, FieldInfo> | undefined {
|
||||
if (!provider.fieldExtractor) return undefined;
|
||||
|
||||
const cacheKey = classNode.startIndex as number;
|
||||
let cached = fieldInfoCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const result = provider.fieldExtractor.extract(classNode, context);
|
||||
if (!result?.fields?.length) return undefined;
|
||||
|
||||
cached = new Map<string, FieldInfo>();
|
||||
for (const field of result.fields) {
|
||||
cached.set(field.name, field);
|
||||
}
|
||||
fieldInfoCache.set(cacheKey, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Enclosing function detection (for call extraction) — cached
|
||||
|
|
@ -1131,7 +1200,18 @@ const processFileGroup = (
|
|||
|
||||
if (routed.kind === 'properties') {
|
||||
const propEnclosingClassId = cachedFindEnclosingClassId(captureMap['call'], file.path);
|
||||
// Enrich routed properties with FieldExtractor metadata
|
||||
let routedFieldMap: Map<string, FieldInfo> | undefined;
|
||||
if (provider.fieldExtractor && typeEnv) {
|
||||
const classNode = findEnclosingClassNode(captureMap['call']);
|
||||
if (classNode) {
|
||||
routedFieldMap = getFieldInfo(classNode, provider, {
|
||||
typeEnv, symbolTable: NOOP_SYMBOL_TABLE, filePath: file.path, language,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const item of routed.items) {
|
||||
const routedFieldInfo = routedFieldMap?.get(item.propName);
|
||||
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
||||
result.nodes.push({
|
||||
id: nodeId,
|
||||
|
|
@ -1144,6 +1224,10 @@ const processFileGroup = (
|
|||
language,
|
||||
isExported: true,
|
||||
description: item.accessorType,
|
||||
...(item.declaredType ? { declaredType: item.declaredType } : routedFieldInfo?.type ? { declaredType: routedFieldInfo.type } : {}),
|
||||
...(routedFieldInfo?.visibility !== undefined ? { visibility: routedFieldInfo.visibility } : {}),
|
||||
...(routedFieldInfo?.isStatic !== undefined ? { isStatic: routedFieldInfo.isStatic } : {}),
|
||||
...(routedFieldInfo?.isReadonly !== undefined ? { isReadonly: routedFieldInfo.isReadonly } : {}),
|
||||
},
|
||||
});
|
||||
result.symbols.push({
|
||||
|
|
@ -1152,7 +1236,10 @@ const processFileGroup = (
|
|||
nodeId,
|
||||
type: 'Property',
|
||||
...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
|
||||
...(item.declaredType ? { declaredType: item.declaredType } : {}),
|
||||
...(item.declaredType ? { declaredType: item.declaredType } : routedFieldInfo?.type ? { declaredType: routedFieldInfo.type } : {}),
|
||||
...(routedFieldInfo?.visibility !== undefined ? { visibility: routedFieldInfo.visibility } : {}),
|
||||
...(routedFieldInfo?.isStatic !== undefined ? { isStatic: routedFieldInfo.isStatic } : {}),
|
||||
...(routedFieldInfo?.isReadonly !== undefined ? { isReadonly: routedFieldInfo.isReadonly } : {}),
|
||||
});
|
||||
const fileId = generateId('File', file.path);
|
||||
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
||||
|
|
@ -1316,6 +1403,9 @@ const processFileGroup = (
|
|||
let parameterTypes: string[] | undefined;
|
||||
let returnType: string | undefined;
|
||||
let declaredType: string | undefined;
|
||||
let visibility: string | undefined;
|
||||
let isStatic: boolean | undefined;
|
||||
let isReadonly: boolean | undefined;
|
||||
if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
|
||||
const sig = extractMethodSignature(definitionNode);
|
||||
parameterCount = sig.parameterCount;
|
||||
|
|
@ -1333,9 +1423,22 @@ const processFileGroup = (
|
|||
}
|
||||
}
|
||||
} else if (nodeLabel === 'Property' && definitionNode) {
|
||||
// Extract the declared type for property/field nodes.
|
||||
// Walk the definition node for type annotation children.
|
||||
declaredType = extractPropertyDeclaredType(definitionNode);
|
||||
// FieldExtractor is the single source of truth when available
|
||||
if (provider.fieldExtractor && typeEnv) {
|
||||
const classNode = findEnclosingClassNode(definitionNode);
|
||||
if (classNode) {
|
||||
const fieldMap = getFieldInfo(classNode, provider, {
|
||||
typeEnv, symbolTable: NOOP_SYMBOL_TABLE, filePath: file.path, language,
|
||||
});
|
||||
const info = fieldMap?.get(nodeName);
|
||||
if (info) {
|
||||
declaredType = info.type ?? undefined;
|
||||
visibility = info.visibility;
|
||||
isStatic = info.isStatic;
|
||||
isReadonly = info.isReadonly;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.nodes.push({
|
||||
|
|
@ -1357,6 +1460,10 @@ const processFileGroup = (
|
|||
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
|
||||
...(parameterTypes !== undefined ? { parameterTypes } : {}),
|
||||
...(returnType !== undefined ? { returnType } : {}),
|
||||
...(declaredType !== undefined ? { declaredType } : {}),
|
||||
...(visibility !== undefined ? { visibility } : {}),
|
||||
...(isStatic !== undefined ? { isStatic } : {}),
|
||||
...(isReadonly !== undefined ? { isReadonly } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1376,6 +1483,9 @@ const processFileGroup = (
|
|||
...(returnType !== undefined ? { returnType } : {}),
|
||||
...(declaredType !== undefined ? { declaredType } : {}),
|
||||
...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
|
||||
...(visibility !== undefined ? { visibility } : {}),
|
||||
...(isStatic !== undefined ? { isStatic } : {}),
|
||||
...(isReadonly !== undefined ? { isReadonly } : {}),
|
||||
});
|
||||
|
||||
const fileId = generateId('File', file.path);
|
||||
|
|
|
|||
|
|
@ -857,6 +857,20 @@ describe('Field type resolution (C++)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.isReadonly).toBe(false);
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1235,6 +1235,21 @@ describe('Field type resolution (C#)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'City');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.declaredType).toBe('string');
|
||||
|
||||
const addr = properties.find(p => p.name === 'Address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -982,6 +982,19 @@ describe('Field type resolution (Go)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('Property nodes contain expected field names', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'City');
|
||||
expect(city).toBeDefined();
|
||||
|
||||
const name = properties.find(p => p.name === 'Name');
|
||||
expect(name).toBeDefined();
|
||||
|
||||
const addr = properties.find(p => p.name === 'Address');
|
||||
expect(addr).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1118,6 +1118,22 @@ describe('Field type resolution (Java)', () => {
|
|||
expect(addressReads[0].source).toBe('processUser');
|
||||
expect(addressReads[0].targetLabel).toBe('Property');
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, isStatic, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.isReadonly).toBe(false);
|
||||
expect(city!.properties.declaredType).toBe('String');
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -268,6 +268,30 @@ describe('Field type resolution (JavaScript)', () => {
|
|||
expect(edgeSet(propEdges)).toContain('Address → city');
|
||||
expect(edgeSet(propEdges)).toContain('Config → DEFAULT');
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, isStatic, isReadonly) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.isReadonly).toBe(false);
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.isStatic).toBe(false);
|
||||
expect(addr!.properties.isReadonly).toBe(false);
|
||||
});
|
||||
|
||||
it('marks Config.DEFAULT as static', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
const def = properties.find(p => p.name === 'DEFAULT');
|
||||
expect(def).toBeDefined();
|
||||
expect(def!.properties.isStatic).toBe(true);
|
||||
expect(def!.properties.visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
// ACCESSES write edges from assignment expressions
|
||||
|
|
|
|||
|
|
@ -1258,6 +1258,19 @@ describe('Field type resolution (Kotlin)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('Property nodes contain expected field names', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
|
||||
const name = properties.find(p => p.name === 'name');
|
||||
expect(name).toBeDefined();
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -1200,6 +1200,21 @@ describe('Field type resolution (PHP)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.declaredType).toBe('string');
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -1444,6 +1444,23 @@ describe('Field type resolution (Python)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, isStatic, isReadonly) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.isReadonly).toBe(false);
|
||||
expect(city!.properties.declaredType).toBe('str');
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.isStatic).toBe(false);
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -888,6 +888,19 @@ describe('Field type resolution (Ruby)', () => {
|
|||
);
|
||||
expect(addressSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('Property nodes contain expected field names', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
|
||||
const name = properties.find(p => p.name === 'name');
|
||||
expect(name).toBeDefined();
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
|
|
@ -1384,6 +1384,23 @@ describe('Field type resolution (Rust)', () => {
|
|||
expect(saveCalls.length).toBe(1);
|
||||
expect(saveCalls[0].targetFilePath).toContain('models');
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, isReadonly, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.isReadonly).toBe(true);
|
||||
expect(city!.properties.declaredType).toBe('String');
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.isReadonly).toBe(true);
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js';
|
||||
|
|
@ -551,6 +551,22 @@ describe.skipIf(!swiftAvailable)('Swift field-type resolution', () => {
|
|||
expect(addressReads[0]!.source).toBe('processUser');
|
||||
expect(addressReads[0]!.targetLabel).toBe('Property');
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
// Swift default visibility is 'internal', not 'public'
|
||||
expect(city!.properties.visibility).toBe('internal');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.declaredType).toBe('String');
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('internal');
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 9: Call-result binding ────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1763,6 +1763,33 @@ describe('Field type resolution (TypeScript)', () => {
|
|||
expect(edge.rel.reason).toBe('read');
|
||||
}
|
||||
});
|
||||
|
||||
it('populates field metadata (visibility, isStatic, isReadonly, declaredType) on Property nodes', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
|
||||
const city = properties.find(p => p.name === 'city');
|
||||
expect(city).toBeDefined();
|
||||
expect(city!.properties.visibility).toBe('public');
|
||||
expect(city!.properties.isStatic).toBe(false);
|
||||
expect(city!.properties.isReadonly).toBe(false);
|
||||
expect(city!.properties.declaredType).toBe('string');
|
||||
|
||||
const addr = properties.find(p => p.name === 'address');
|
||||
expect(addr).toBeDefined();
|
||||
expect(addr!.properties.visibility).toBe('public');
|
||||
expect(addr!.properties.isStatic).toBe(false);
|
||||
expect(addr!.properties.isReadonly).toBe(false);
|
||||
expect(addr!.properties.declaredType).toBe('Address');
|
||||
});
|
||||
|
||||
it('marks Config.DEFAULT as static', () => {
|
||||
const properties = getNodesByLabelFull(result, 'Property');
|
||||
const def = properties.find(p => p.name === 'DEFAULT');
|
||||
expect(def).toBeDefined();
|
||||
expect(def!.properties.isStatic).toBe(true);
|
||||
expect(def!.properties.declaredType).toBe('Config');
|
||||
expect(def!.properties.visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue