fix Julia tests

This commit is contained in:
Dongdong Kong 2026-05-24 19:54:54 +08:00
parent 0444e41f69
commit 8703bfeeb5
6 changed files with 160 additions and 40 deletions

View file

@ -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;
},
};

View file

@ -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');

View file

@ -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');

View file

@ -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,

View 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

View file

@ -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']);
});
});