Skip to content

fix(java-extractor): strip inner call args from chained method-call callee#443

Open
tirth8205 wants to merge 1 commit into
Egonex-AI:mainfrom
tirth8205:fix/java-extractor-chained-call
Open

fix(java-extractor): strip inner call args from chained method-call callee#443
tirth8205 wants to merge 1 commit into
Egonex-AI:mainfrom
tirth8205:fix/java-extractor-chained-call

Conversation

@tirth8205

Copy link
Copy Markdown
Contributor

Problem

JavaExtractor.extractMethodInvocationName builds the callee for a qualified call as ${objectNode.text}.${nameNode.text}. When the receiver (the object field) is itself a method_invocation — i.e. a chained / fluent / builder-style call such as repository.findAll().forEach(handler) or builder().build()objectNode.text is the full source text of the inner call including its parentheses and arguments.

The result is a malformed callee string that embeds () and argument text, e.g.:

  • builder().build() -> callee "builder().build"
  • repository.findAll().forEach(handler) -> callee "repository.findAll().forEach"

A call-graph callee should be a method name (optionally qualified by a simple receiver), never a string containing (). This pollutes the call graph with un-resolvable, parenthesis-laden callee names for any fluent/builder/stream-style Java code. The existing qualified-call test only passes because System.out.println has a field_access object (System.out), not a method_invocation object.

Fix

Guard the qualified-call branch so it only prefixes the receiver when the object is not itself a method_invocation:

const objectNode = node.childForFieldName("object");
if (objectNode && objectNode.type !== "method_invocation") {
  return `${objectNode.text}.${nameNode.text}`;
}
return nameNode.text;

This keeps System.out.println (object type field_access) and plain calls unchanged, and turns builder().build() into callee build (the inner builder() call still emits its own well-formed entry).

Testing

  • Added a test under extractCallGraph that parses builder().build(); and asserts a callee "build" exists and that no callee contains "()". This test fails before the fix (the outer call's callee is the malformed "builder().build") and passes after.
  • The full core Vitest suite is green (693 tests), confirming no regressions to existing extractors.
  • tsc --noEmit on packages/core exits 0 and ESLint on both changed files is clean.

🤖 Generated with Claude Code

…allee

For a chained call like `builder().build()`, the method_invocation's
`object` field is itself a method_invocation node, so building the callee
as `${objectNode.text}.${nameNode.text}` produced the malformed callee
"builder().build" (parentheses and inner args embedded in the name).

Guard the qualified-call branch so it only prefixes the receiver when the
object is not itself a method_invocation. Chained calls now yield a clean
method name ("build") for the outer call plus the existing well-formed
entry for the inner call ("builder"). Simple receivers such as
`System.out.println` (object type field_access) are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@thejesh23 thejesh23 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. Other receiver node types still leak () into the callee.
The guard only excludes method_invocation, but new Foo().bar() (object type object_creation_expression), ((Foo) x).bar() (parenthesized_expression), and (Foo) x.bar() (cast_expression) all have .text containing ()/cast tokens. The PR's own invariant callee.includes("()") === false will be violated by any of these. Consider an allowlist of "simple" receiver types (identifier / field_access / scoped_identifier / this / super) instead of a single-type denylist.

2. super.foo() and explicit-type-args calls aren't covered by tests.
super.foo() (object type super) and obj.<T>foo() / Foo.<T>bar() (type_arguments between object and name) are common in Java but untested here; please add at least a super.bar() assertion so a future grammar/refactor doesn't silently break qualified-super callees.

3. Dropping the receiver hurts call-graph disambiguation.
Replacing builder().build with bare build collapses every fluent terminal into one node, losing the receiver type entirely; downstream consumers can no longer distinguish a.build() vs b.build(). A receiver-typed form like <chain>.build or recursing into the inner method_invocation to extract its name (e.g. builder.build) would preserve more signal. Worth noting since #435's Dart extractor will hit the same chained/cascade shape.

Nit: the new test only asserts presence of "build" and absence of "()"; an explicit assertion that the inner "builder" callee is also emitted would lock in the "inner call still emits its own entry" claim from the PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants