Skip to content

feat(java): no name guess for calls on a proven-external receiver - #2667

Open
tfactor2 wants to merge 1 commit into
DeusData:mainfrom
tfactor2:java-external-gate
Open

tfactor2 wants to merge 1 commit into
DeusData:mainfrom
tfactor2:java-external-gate

Conversation

@tfactor2

Copy link
Copy Markdown

What does this PR do?

Fixes #2660. When the Java resolver knows a receiver's type by its package-qualified name, and no project type lives in that package (JDK, libraries), the call gets lsp_external. Both pipeline paths then look it up strictly by QN, never by name (as Rust does since #2339).

Unknown types, and types missing from a project package, keep the existing guess. The sequential path now builds the Java cross registry too, so both paths agree.

Known gap: List.get and List.isEmpty still get a guessed edge.

On a 3,178-file Spring service, this and the follow-ups for #2382 and #2662 remove 126 of 821 sampled edges; 12 of 15 checked were wrong, none right.

Checklist

  • Every commit is signed off (git commit -s)
  • Tests pass locally: full C runner 9,705 passed; its 4 failures fail on main too. make test stops earlier at the version-metadata contract, as on main.
  • Lint passes: lint-no-suppress, lint-memory-core pass; cppcheck and clang-format not installed here
  • New behavior is covered by a test: java_typed_calls (fails on main), parallel_java_external_target_contract

🤖 Generated with Claude Code

When the Java resolver resolves a call's receiver to a type outside the
project (a JDK package, or a package no project file declares), the call
gets strategy lsp_external. Both pipeline paths (pass_calls.c and
pass_parallel.c) then look the target up strictly by QN through
cbm_pipeline_java_external_target and skip the registry's name guess, so
no edge is created to an unrelated project method of the same name. This
is the rule the Rust path follows since DeusData#2053.

Positive proof only: a receiver of unknown type keeps the existing guess,
and a seeded JDK type that lacks the called method stays no_method_match.
Type variables (T, ID) are never taken for package-qualified types.
A package counts as the project's when any registry layer holds a
project type in it (the layer above the shared registry is a per-file
overlay), and a path-derived spelling counts as the project's also for
a file at the repository root. java_receiver_is_external memoizes its
answer per package.

The sequential path now builds the Java cross registry, as the parallel
path does, so the gate applies on both.

Tests: java_typed_calls (external receivers get no edge; a project
receiver keeps its edge, and so does one whose type is missing from a
project package), parallel_java_external_target_contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Timur Nizametdinov <7229951+tfactor2@users.noreply.github.com>
@tfactor2
tfactor2 requested a review from DeusData as a code owner October 16, 2026 17:47
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thank you very much for this, @tfactor2. It is a careful and well-reasoned change, and it removes a whole class of fabricated edges that has bothered us for a long time. We measured it properly before replying, and the numbers are very good.

What we measured. We merged the PR head (0ec6967) onto current main and indexed 15 open-source Java repositories with and without it on the parallel resolver, plus 6 of them on the sequential one. We hand-judged a stratified sample of the changed edges against the source.

  • Parallel, 15 repos: CALLS 749,216 → 665,992, i.e. −83,225 (−11.1 %), with 1 edge added. About 96 % of the sampled removals were wrong edges, for example:
    • Thread.sleep, Calendar.getInstance and Map.get bound to same-named project methods;
    • new Date() / new ArrayList<>() bound to a pom.xml element;
    • String.format bound to a project format.
      The remaining ~3–4 % were calls on a JDK interface type whose project implementation happened to be the guessed target (ParameterizedType.getOwnerType() → Utils.ParameterizedTypeImpl.getOwnerType). That trade-off is a broader design question on our side; nothing for you to change here.
  • Sequential, 6 repos: CALLS −5,079 / +680. The additions come from the new Tier-2 registry line for small repositories and were correct in every sampled case: calls on project interfaces now reach the interface method, and constructor calls reach the constructor.

Two small requests before we merge:

1. Own package (and the file's own types) before java.lang. java.lang is an implicit on-demand import (JLS §7.3), and an on-demand import never shadows a type of the compilation unit's own package (§6.4.1, §7.5.2). The resolver checks java.lang first. Before this PR, that wrong first answer was harmless because the textual fallback still found the project class. Now it marks the receiver as external and the edge is dropped. Minimal repro (both resolver paths, with 50+ files so the shared registry is built):

// src/main/java/com/acme/protogen/Enum.java
package com.acme.protogen;
public class Enum {
    public Enum(Class<?> cls, String parent) {}
}

// src/main/java/com/acme/protogen/Message.java
package com.acme.protogen;
public class Message {
    public void addNestedEnum(Class<?> cls) {
        Enum e = new Enum(cls, "parent");   // expected: CALLS -> com.acme.protogen.Enum constructor
    }
}

On main this produces addNestedEnum -> com.acme.protogen.Enum.Enum. With the PR the edge is gone, because Enum resolves to java.lang.Enum and is classified as external. The same shape occurs in Netflix/conductor (protogen.Enum). Project classes named Process, Module, Record, Override and so on are affected the same way. Could you move the own-package lookup ahead of java.lang in the type-name resolution and add a test like the one above?

2. PR description. Please restate the numbers for this PR alone. As far as we can tell, the current figures include the follow-ups you mentioned for #2382 and #2662. Please also call out the pass_lsp_cross.c line that builds the Java shared registry on the sequential path, or split it into its own commit. It is a good change, but it is a behaviour change for every repository of 50 files or fewer, and reviewers should see it as such.

Thanks again; we are looking forward to merging this.

This branch has not been deployed

No deployments
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.

Java: a call on a JDK-typed receiver binds a same-named project method

2 participants