mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-29 01:41:42 +00:00
fix Julia tests
This commit is contained in:
parent
0444e41f69
commit
8703bfeeb5
6 changed files with 160 additions and 40 deletions
|
|
@ -1,47 +1,71 @@
|
|||
// gitnexus/src/core/ingestion/field-extractors/configs/julia.ts
|
||||
// Verified against tree-sitter-julia grammar
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
|
||||
/**
|
||||
* Julia field extraction config.
|
||||
* Extract the struct name from a struct_definition node.
|
||||
*
|
||||
* Julia struct fields appear as typed or untyped identifiers inside struct bodies:
|
||||
* struct Point
|
||||
* x::Float64
|
||||
* y::Float64
|
||||
* end
|
||||
*
|
||||
* In tree-sitter-julia the struct body contains field declarations.
|
||||
* Each field is either an identifier or a typed_parameter node.
|
||||
* Julia grammar has no named fields on struct_definition, so
|
||||
* childForFieldName('name') always returns null. The name lives at:
|
||||
* struct_definition > type_head > identifier (simple)
|
||||
* struct_definition > type_head > binary_expression > identifier (with <: supertype)
|
||||
*/
|
||||
function extractStructOwnerName(node: SyntaxNode): string | undefined {
|
||||
const typeHead = node.namedChild(0);
|
||||
if (typeHead?.type !== 'type_head') return undefined;
|
||||
const first = typeHead.firstNamedChild;
|
||||
if (first?.type === 'identifier') return first.text;
|
||||
// struct Foo <: Bar → binary_expression first child is the name identifier
|
||||
if (first?.type === 'binary_expression') return first.firstNamedChild?.text;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract field name + type from a struct field node.
|
||||
*
|
||||
* Handles three shapes that appear as direct children of struct_definition:
|
||||
* identifier → plain untyped field: `label`
|
||||
* typed_expression → typed field: `x::Float64`
|
||||
* assignment → field with default (@with_kw): `x::Int = 1` or `flag = false`
|
||||
*
|
||||
* The `type_head` child (struct name / supertype) is excluded via fieldNodeTypes.
|
||||
*/
|
||||
function extractFieldName(node: SyntaxNode): string | undefined {
|
||||
if (node.type === 'identifier') return node.text;
|
||||
if (node.type === 'typed_expression') return node.firstNamedChild?.text;
|
||||
if (node.type === 'assignment') {
|
||||
const lhs = node.firstNamedChild;
|
||||
if (lhs?.type === 'typed_expression') return lhs.firstNamedChild?.text;
|
||||
if (lhs?.type === 'identifier') return lhs.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractFieldType(node: SyntaxNode): string | undefined {
|
||||
if (node.type === 'typed_expression') return node.namedChild(1)?.text?.trim();
|
||||
if (node.type === 'assignment') {
|
||||
const lhs = node.firstNamedChild;
|
||||
if (lhs?.type === 'typed_expression') return lhs.namedChild(1)?.text?.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const juliaConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Julia,
|
||||
typeDeclarationNodes: ['struct_definition'],
|
||||
fieldNodeTypes: ['identifier', 'typed_parameter'],
|
||||
bodyNodeTypes: ['field_declaration_list'],
|
||||
// Fields are direct children of struct_definition (no wrapper body node).
|
||||
// type_head holds the struct name and is excluded by listing only field shapes.
|
||||
fieldNodeTypes: ['identifier', 'typed_expression', 'assignment'],
|
||||
bodyNodeTypes: [],
|
||||
useOwnerAsBody: true,
|
||||
extractOwnerName: extractStructOwnerName,
|
||||
defaultVisibility: 'public',
|
||||
|
||||
extractName(node) {
|
||||
if (node.type === 'identifier') return node.text;
|
||||
if (node.type === 'typed_parameter') {
|
||||
const nameNode = node.childForFieldName('name') ?? node.firstNamedChild;
|
||||
return nameNode?.type === 'identifier' ? nameNode.text : undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
if (node.type === 'typed_parameter') {
|
||||
const typeNode = node.childForFieldName('type') ?? node.namedChild(1);
|
||||
return typeNode?.text?.trim();
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
extractName: extractFieldName,
|
||||
extractType: extractFieldType,
|
||||
|
||||
extractVisibility(_node) {
|
||||
// Julia struct fields are always public (no access modifiers)
|
||||
return 'public';
|
||||
},
|
||||
|
||||
|
|
@ -50,9 +74,6 @@ export const juliaConfig: FieldExtractionConfig = {
|
|||
},
|
||||
|
||||
isReadonly(_node) {
|
||||
// Immutable structs (struct, not mutable struct) have readonly fields.
|
||||
// The mutable keyword is on the parent struct_definition, not on individual fields.
|
||||
// We return false here; the struct-level immutability is handled elsewhere.
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,6 +56,18 @@ export interface FieldExtractionConfig {
|
|||
/** Extract fields from primary constructor parameters on the owner node itself
|
||||
* (e.g. C# record positional parameters, C# 12 class primary constructors). */
|
||||
extractPrimaryFields?: (ownerNode: SyntaxNode, context: FieldExtractorContext) => FieldInfo[];
|
||||
/**
|
||||
* Custom owner name extractor for grammars where `childForFieldName('name')` does not work
|
||||
* (e.g. Julia, where struct_definition has no named grammar fields).
|
||||
* When provided, used instead of `node.childForFieldName('name')?.text`.
|
||||
*/
|
||||
extractOwnerName?: (node: SyntaxNode) => string | undefined;
|
||||
/**
|
||||
* When true, use the owner node itself as the field body (fields are direct children).
|
||||
* For grammars like Julia where struct fields live directly under the type-declaration node
|
||||
* rather than inside a separate body wrapper node.
|
||||
*/
|
||||
useOwnerAsBody?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -85,9 +97,8 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
|
|||
if (!this.isTypeDeclaration(node)) return null;
|
||||
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return null;
|
||||
|
||||
const ownerFqn = nameNode.text;
|
||||
const ownerFqn = config.extractOwnerName?.(node) ?? nameNode?.text;
|
||||
if (!ownerFqn) return null;
|
||||
const fields: FieldInfo[] = [];
|
||||
|
||||
// Find body container(s)
|
||||
|
|
@ -110,6 +121,7 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
|
|||
// ------------------------------------------------------------------
|
||||
|
||||
private findBodies(node: SyntaxNode): SyntaxNode[] {
|
||||
if (config.useOwnerAsBody) return [node];
|
||||
const result: SyntaxNode[] = [];
|
||||
// Try named 'body' field first
|
||||
const bodyField = node.childForFieldName('body');
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ function extractJuliaParameters(node: SyntaxNode): ParameterInfo[] {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'typed_parameter': {
|
||||
case 'typed_parameter':
|
||||
case 'typed_expression': {
|
||||
const nameNode = param.childForFieldName('name') ?? param.firstNamedChild;
|
||||
const typeNode = param.childForFieldName('type') ?? param.namedChild(1);
|
||||
if (nameNode) {
|
||||
|
|
@ -66,6 +67,33 @@ function extractJuliaParameters(node: SyntaxNode): ParameterInfo[] {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'named_argument': {
|
||||
// Default positional (x::Int=1) or keyword (; kw::String="hello") parameter.
|
||||
// First named child is typed_expression (typed) or identifier (untyped).
|
||||
const first = param.firstNamedChild;
|
||||
if (first?.type === 'typed_expression') {
|
||||
const nameNode = first.firstNamedChild;
|
||||
const typeNode = first.namedChild(1);
|
||||
if (nameNode) {
|
||||
params.push({
|
||||
name: nameNode.text,
|
||||
type: typeNode?.text?.trim() ?? null,
|
||||
rawType: typeNode?.text?.trim() ?? null,
|
||||
isOptional: true,
|
||||
isVariadic: false,
|
||||
});
|
||||
}
|
||||
} else if (first?.type === 'identifier') {
|
||||
params.push({
|
||||
name: first.text,
|
||||
type: null,
|
||||
rawType: null,
|
||||
isOptional: true,
|
||||
isVariadic: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'optional_parameter': {
|
||||
const nameNode = param.childForFieldName('name') ?? param.firstNamedChild;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
|
|
|
|||
|
|
@ -151,9 +151,7 @@ export function buildMethodProps(info: MethodInfo): Record<string, unknown> {
|
|||
}
|
||||
return {
|
||||
parameterCount: hasVariadic ? undefined : info.parameters.length,
|
||||
...(!hasVariadic && optionalCount > 0
|
||||
? { requiredParameterCount: info.parameters.length - optionalCount }
|
||||
: {}),
|
||||
...(!hasVariadic ? { requiredParameterCount: info.parameters.length - optionalCount } : {}),
|
||||
...(types.length > 0 ? { parameterTypes: types } : {}),
|
||||
returnType: info.returnType ?? undefined,
|
||||
visibility: info.visibility,
|
||||
|
|
|
|||
21
gitnexus/test/fixtures/lang-resolution/julia-with-kw/app.jl
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/julia-with-kw/app.jl
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Parameters
|
||||
|
||||
@with_kw struct ModelConfig
|
||||
hidden_size::Int = 256
|
||||
dropout::Float64 = 0.1
|
||||
label::String = "default"
|
||||
verbose = false
|
||||
end
|
||||
|
||||
struct Point
|
||||
x::Float64
|
||||
y::Float64
|
||||
end
|
||||
|
||||
function train(cfg::ModelConfig; epochs::Int=10, lr::Float64=0.001)
|
||||
return cfg.hidden_size
|
||||
end
|
||||
|
||||
function distance(a::Point, b::Point)
|
||||
return a.x - b.x
|
||||
end
|
||||
|
|
@ -173,3 +173,43 @@ describe('Julia export visibility in top-level scripts', () => {
|
|||
expect(api?.properties.isExported).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameters.jl @with_kw — struct field extraction + keyword/default params
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Julia @with_kw struct and keyword parameter extraction', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'julia-with-kw'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('detects @with_kw struct as a Class node', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('ModelConfig');
|
||||
});
|
||||
|
||||
it('detects plain struct as a Class node', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes).toContain('Point');
|
||||
});
|
||||
|
||||
it('extracts keyword/default parameters for train(cfg; epochs, lr)', () => {
|
||||
const fns = getNodesByLabelFull(result, 'Function');
|
||||
const train = fns.find((n) => n.name === 'train');
|
||||
expect(train).toBeDefined();
|
||||
expect(train?.properties.parameterCount).toBe(3);
|
||||
expect(train?.properties.requiredParameterCount).toBe(1);
|
||||
expect(train?.properties.parameterTypes).toEqual(['ModelConfig', 'Int', 'Float64']);
|
||||
});
|
||||
|
||||
it('extracts required-only parameters for distance(a, b)', () => {
|
||||
const fns = getNodesByLabelFull(result, 'Function');
|
||||
const dist = fns.find((n) => n.name === 'distance');
|
||||
expect(dist).toBeDefined();
|
||||
expect(dist?.properties.parameterCount).toBe(2);
|
||||
expect(dist?.properties.requiredParameterCount).toBe(2);
|
||||
expect(dist?.properties.parameterTypes).toEqual(['Point', 'Point']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue