diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index e3b65379eb..11fa8c92c9 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -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." } } diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/jvm.ts b/gitnexus/src/core/ingestion/method-extractors/configs/jvm.ts index df7476ceb2..3233dc9386 100644 --- a/gitnexus/src/core/ingestion/method-extractors/configs/jvm.ts +++ b/gitnexus/src/core/ingestion/method-extractors/configs/jvm.ts @@ -176,6 +176,16 @@ export const javaMethodConfig: MethodExtractionConfig = { const KOTLIN_VIS = new Set(['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, }); } diff --git a/gitnexus/test/unit/kotlin-scope-captures.test.ts b/gitnexus/test/unit/kotlin-scope-captures.test.ts index bba271ac0f..4639ec4a2d 100644 --- a/gitnexus/test/unit/kotlin-scope-captures.test.ts +++ b/gitnexus/test/unit/kotlin-scope-captures.test.ts @@ -8,6 +8,26 @@ function captureTexts(source: string): Array> { } 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) { diff --git a/gitnexus/test/unit/method-extraction.test.ts b/gitnexus/test/unit/method-extraction.test.ts index f13dfbd042..452ebc0bd2 100644 --- a/gitnexus/test/unit/method-extraction.test.ts +++ b/gitnexus/test/unit/method-extraction.test.ts @@ -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(`