fix(kotlin): detect default parameter arity (#2034)

* fix(kotlin): detect default parameter arity

* test(kotlin): rebaseline optional arity captures

* test(kotlin): cover default parameter boundaries
This commit is contained in:
azizur100389 2026-06-04 17:28:04 +01:00 • committed by GitHub
parent 479b8146b8
commit 2ce95d6e42
4 changed files with 81 additions and 13 deletions

View file

@ -74,9 +74,9 @@
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged."
},
"kotlin": {
"fingerprint": "9b212eca24959cc1705933df213f4ec739c5c1b4580c7929347d37bf4d72ba8a",
"fingerprint": "a16400622892183581b8f5f8fa01f07842d19b8cf49ed021df52bd17009d749f",
"scaling_budget": 1.5,
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (: Base()); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
"_rebaselined": "#1956 synth-widening: + kotlin-qualified-base fixture; synthesizeKotlinInheritanceReferences now handles the explicit_delegation form (class F : Iface by d -> Iface), matching the #1940 legacy leg, at parity. Linear (~0.87). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged."
"_rebaselined": "#1956 synth-widening: + kotlin-qualified-base fixture; synthesizeKotlinInheritanceReferences now handles the explicit_delegation form (class F : Iface by d -> Iface), matching the #1940 legacy leg, at parity. Linear (~0.87). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1930 F45: default parameters now emit optional-arity metadata; capture fingerprint changes, scaling remains linear."
}
}

View file

@ -176,6 +176,16 @@ export const javaMethodConfig: MethodExtractionConfig = {
const KOTLIN_VIS = new Set<MethodVisibility>(['public', 'private', 'protected', 'internal']);
function kotlinParameterHasDefaultValue(param: SyntaxNode): boolean {
// tree-sitter-kotlin inlines `_function_value_parameter`, so `=` and its
// expression are siblings of `parameter`, not children of the parameter.
for (let sibling = param.nextSibling; sibling !== null; sibling = sibling.nextSibling) {
if (sibling.type === ',' || sibling.type === ')') return false;
if (sibling.type === '=') return true;
}
return false;
}
function extractKotlinParameters(node: SyntaxNode): ParameterInfo[] {
const params: ParameterInfo[] = [];
// Kotlin: function_declaration > function_value_parameters > parameter
@ -199,7 +209,6 @@ function extractKotlinParameters(node: SyntaxNode): ParameterInfo[] {
let paramName: string | undefined;
let paramType: string | null = null;
let paramRawType: string | null = null;
let hasDefault = false;
const isVariadic = nextIsVariadic;
nextIsVariadic = false;
@ -218,21 +227,12 @@ function extractKotlinParameters(node: SyntaxNode): ParameterInfo[] {
}
}
// Check for default value: `= expr`
for (let k = 0; k < param.childCount; k++) {
const c = param.child(k);
if (c && c.text === '=') {
hasDefault = true;
break;
}
}
if (paramName) {
params.push({
name: paramName,
type: paramType,
rawType: paramRawType,
isOptional: hasDefault,
isOptional: kotlinParameterHasDefaultValue(param),
isVariadic: isVariadic,
});
}

View file

@ -8,6 +8,26 @@ function captureTexts(source: string): Array<Record<string, string>> {
}
describe('Kotlin scope captures', () => {
it('emits required arity equal to total arity when no parameters have defaults', () => {
const captures = captureTexts(`
fun greet(name: String, greeting: String) {}
`);
const declaration = captures.find((match) => match['@declaration.function'] !== undefined);
expect(declaration?.['@declaration.parameter-count']).toBe('2');
expect(declaration?.['@declaration.required-parameter-count']).toBe('2');
});
it('emits required arity excluding default parameters', () => {
const captures = captureTexts(`
fun greet(name: String, greeting: String = "Hello", punctuation: String = "!") {}
`);
const declaration = captures.find((match) => match['@declaration.function'] !== undefined);
expect(declaration?.['@declaration.parameter-count']).toBe('3');
expect(declaration?.['@declaration.required-parameter-count']).toBe('1');
});
it('counts trailing lambda call suffixes as call arguments', () => {
const captures = captureTexts(`
fun run(items: List<String>) {

View file

@ -504,6 +504,54 @@ describeKotlin('Kotlin MethodExtractor', () => {
});
});
describe('default parameters', () => {
it('keeps all required parameters non-optional', () => {
const tree = parseKotlin(`
class Greeter {
fun greet(name: String, greeting: String) { }
}
`);
const classNode = tree.rootNode.child(0)!;
const result = extractor.extract(classNode, kotlinCtx);
expect(result!.methods[0].parameters.map((parameter) => parameter.isOptional)).toEqual([
false,
false,
]);
});
it('marks parameters with default expressions as optional', () => {
const tree = parseKotlin(`
class Greeter {
fun greet(name: String, greeting: String = "Hello", punctuation: String = "!") { }
}
`);
const classNode = tree.rootNode.child(0)!;
const result = extractor.extract(classNode, kotlinCtx);
expect(result!.methods[0].parameters.map((parameter) => parameter.isOptional)).toEqual([
false,
true,
true,
]);
});
it('preserves a required parameter after a defaulted parameter', () => {
const tree = parseKotlin(`
class Greeter {
fun greet(greeting: String = "Hello", name: String) { }
}
`);
const classNode = tree.rootNode.child(0)!;
const result = extractor.extract(classNode, kotlinCtx);
expect(result!.methods[0].parameters.map((parameter) => parameter.isOptional)).toEqual([
true,
false,
]);
});
});
describe('extension functions', () => {
it('extracts receiverType for extension functions', () => {
const tree = parseKotlin(`