diff --git a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts index d58f4981c..68b01e421 100644 --- a/gitnexus/src/core/ingestion/languages/kotlin/captures.ts +++ b/gitnexus/src/core/ingestion/languages/kotlin/captures.ts @@ -95,19 +95,25 @@ const KOTLIN_CALLABLE_CAPTURE_OPTIONS = { const isKotlinComment = (node: SyntaxNode): boolean => node.type === 'line_comment' || node.type === 'multiline_comment'; +const namedNonComments = (node: SyntaxNode): SyntaxNode[] => + node.namedChildren.filter( + (child): child is SyntaxNode => child !== null && !isKotlinComment(child), + ); + /** * Branches of a Kotlin value-selecting expression (#3354). `a ?: b` is a * FIELDLESS `elvis_expression` (positional operands), so the shared * field-based rule never saw it and only the last operand flowed. An * `if_expression` fields its branches as `control_structure_body` wrappers: - * a wrapper holding one expression is that expression's value, anything - * else (a multi-statement block) keeps the whole `if` one opaque source. + * a bare branch (`if (c) ::f`) holds the expression directly, a braced one + * (`if (c) { ::f }`) nests it one level deeper in a `statements` node. A + * branch holding exactly one expression is that expression's value; an + * empty or multi-statement block, or an `if` without `else`, keeps the + * whole `if` one opaque source. */ function kotlinValueAlternatives(node: SyntaxNode): readonly SyntaxNode[] | undefined { if (node.type === 'elvis_expression') { - const operands = node.namedChildren.filter( - (child): child is SyntaxNode => child !== null && !isKotlinComment(child), - ); + const operands = namedNonComments(node); return operands.length === 2 ? operands : undefined; } if (node.type !== 'if_expression') return undefined; @@ -115,9 +121,9 @@ function kotlinValueAlternatives(node: SyntaxNode): readonly SyntaxNode[] | unde for (const field of ['consequence', 'alternative'] as const) { const body = node.childForFieldName(field); if (body === null) return [node]; - const values = body.namedChildren.filter( - (child): child is SyntaxNode => child !== null && !isKotlinComment(child), - ); + let values = namedNonComments(body); + const [block] = values; + if (values.length === 1 && block?.type === 'statements') values = namedNonComments(block); const [value] = values; if (values.length !== 1 || value === undefined) return [node]; branches.push(value); diff --git a/gitnexus/test/fixtures/lang-resolution/kotlin-callable-alternatives/App.kt b/gitnexus/test/fixtures/lang-resolution/kotlin-callable-alternatives/App.kt index 58bef2c21..b7b406a85 100644 --- a/gitnexus/test/fixtures/lang-resolution/kotlin-callable-alternatives/App.kt +++ b/gitnexus/test/fixtures/lang-resolution/kotlin-callable-alternatives/App.kt @@ -11,3 +11,20 @@ fun ifExpression(fast: Boolean) { val run = if (fast) ::runThen else ::runElse run() } + +fun runBracedThen() {} +fun runBracedElse() {} + +fun braced(fast: Boolean) { + val run = if (fast) { ::runBracedThen } else { ::runBracedElse } + run() +} + +fun log() {} +fun runBlockThen() {} +fun runBlockElse() {} + +fun multiStatement(fast: Boolean) { + val run = if (fast) { log(); ::runBlockThen } else { ::runBlockElse } + run() +} diff --git a/gitnexus/test/integration/resolvers/callable-alternatives-providers.test.ts b/gitnexus/test/integration/resolvers/callable-alternatives-providers.test.ts index cace93f5d..dbcecde35 100644 --- a/gitnexus/test/integration/resolvers/callable-alternatives-providers.test.ts +++ b/gitnexus/test/integration/resolvers/callable-alternatives-providers.test.ts @@ -62,6 +62,18 @@ describe('Kotlin callable chosen by `?:` / `if` expression', () => { expect.arrayContaining(['ifExpression → runThen', 'ifExpression → runElse']), ); }); + + // A braced branch nests its value in a `statements` node one level below + // the `control_structure_body` wrapper. + it('`if (c) { ::f } else { ::g }` reaches both branches', () => { + expect(callsOf(result)).toEqual( + expect.arrayContaining(['braced → runBracedThen', 'braced → runBracedElse']), + ); + }); + + it('a multi-statement branch keeps the whole `if` opaque', () => { + expect(callsOf(result).filter((edge) => edge.startsWith('multiStatement →'))).toEqual([]); + }); }); describe('Swift callable chosen by `??` / `?:`', () => {