Skip to content

fix(lsp): callHierarchy/incomingCalls returns top-level/script callers (#3093) - #3191

Merged
EffortlessSteven merged 2 commits into
mainfrom
fix/3093-incoming-top-level-caller
Jun 30, 2026
Merged

fix(lsp): callHierarchy/incomingCalls returns top-level/script callers (#3093)#3191
EffortlessSteven merged 2 commits into
mainfrom
fix/3093-incoming-top-level-caller

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Diagnosis

incomingCalls for App::run returned [] even though script/real-baseline.pl calls $app->run at the top level (not inside a sub). Both code paths silently dropped callers with no enclosing callable:

  1. Workspace-index path (hierarchy.rs:632-650): find_workspace_enclosing_callable returned None for a top-level ref → the ref was dropped inside if let Some(from) = ....
  2. Open-doc fallback (call_hierarchy_provider/mod.rs): FunctionCall/MethodCall arms guarded with if let Some(from) = current_function → top-level sites where current_function = None produced no output.

Step 0 verification: the workspace index DOES record top-level MethodCall references (line 4020 of workspace_index.rs stores bare method names unconditionally). The reference was in the index; the bug was in the consumer, not the recorder.

Fix

crates/perl-lsp-rs/src/call_hierarchy_provider/mod.rs

  • Added uri_basename(uri: &str) -> String helper to extract the filename from a URI.
  • FunctionCall and MethodCall arms: replaced if let Some(from) = current_function with current_function.cloned().unwrap_or_else(|| /* synthesize file-level item */) — so top-level call sites produce a CallHierarchyItem { kind: "file", name: basename, uri, ... }.
  • Subroutine arm: added return after visiting children inside a named sub, preventing a pre-existing double-visit that would have caused spurious file-level callers for calls inside subs.
  • to_json(): added "file" => 1 mapping (LSP SymbolKind.File = 1).

crates/perl-lsp-rs/src/runtime/language/hierarchy.rs

  • Workspace-index for location in refs loop: replaced if let Some(from) = find_workspace_enclosing_callable(...) with .unwrap_or_else(|| /* synthesize file-level item */).

crates/perl-lsp-ux-tests/tests/ux_scenario_22_crossfile_extended_providers.rs

  • Un-ignored scenario_22_call_hierarchy_incoming_to_run_hard_assert.
  • Added assertion that at least one caller's URI contains "real-baseline.pl" (non-vacuous guard).

Before / After Test Evidence

Before fix (with #[ignore] removed but no fix): test would FAIL with [] result.

After fix (UX_TEST_EXIT=0):

running 1 test
test scenario_22_call_hierarchy_incoming_to_run_hard_assert ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 19 filtered out; finished in 3.35s
UX_TEST_EXIT=0

Verification

  • cargo build -p perl-lsp-rs --bin perl-lsp: BUILD_EXIT=0
  • cargo test -p perl-lsp-rs --lib -- call_hierarchy: 5/5 ok, TEST_EXIT=0
  • cargo clippy -p perl-lsp-rs --locked -- -D warnings -A missing_docs: CLIPPY_EXIT=0
  • cargo fmt --check -p perl-lsp-rs: FMT_RS_EXIT=0
  • cargo fmt --check -p perl-lsp-ux-tests: FMT_UX_EXIT=0
  • Integration test: scenario_22_call_hierarchy_incoming_to_run_hard_assert PASSED (EXIT=0)

🤖 Generated with Claude Code

https://claude.ai/code/session_015AGtUiPPvBkWTeN9nm6q6N

#3093)

When a call site has no enclosing `sub` (i.e., it is top-level in a script),
both the workspace-index path and the open-doc fallback silently dropped the
reference. This made `$app->run` in script/real-baseline.pl invisible to
incomingCalls for App::run.

Fix: synthesize a file-level CallHierarchyItem (kind=File/1, name=basename)
for each call site that has no enclosing callable, in both paths:
- hierarchy.rs: workspace-index path now uses unwrap_or_else to create the
  synthetic item instead of wrapping the whole body in if-let-Some.
- call_hierarchy_provider/mod.rs: FunctionCall and MethodCall arms now call
  current_function.cloned().unwrap_or_else(|| <file-level item>) instead of
  guarding with if-let-Some(from).

Also adds uri_basename() helper and fixes a pre-existing double-visit bug in
find_incoming_calls: named-sub arm now returns early after visiting children
so the bottom visitor doesn't re-visit with the outer (None) context, which
would have created spurious file-level callers for calls inside subs.

Adds SymbolKind.File (1) mapping to CallHierarchyItem::to_json().

Test: un-ignores scenario_22_call_hierarchy_incoming_to_run_hard_assert and
strengthens it to assert the caller URI contains "real-baseline.pl".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AGtUiPPvBkWTeN9nm6q6N
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes
    • Call hierarchy now correctly reports top-level calls that aren’t inside any named subroutine, so cross-file incoming callers are no longer dropped.
    • Improved traversal avoids misattributing file-level callers when references occur within named subroutines.
    • Call hierarchy items now show a file-based caller entry for top-level call sites.
  • Tests
    • Re-enabled and strengthened the cross-file incoming-calls scenario to assert the expected caller is present.

Walkthrough

Adds a file-level caller fallback for top-level incoming-call matches in both the AST-based and workspace-index call hierarchy paths, maps the new file kind to LSP symbol kind 1, and updates unit and UX tests to assert the new behavior.

Changes

Top-level caller synthesis for callHierarchy/incomingCalls

Layer / File(s) Summary
AST provider: uri_basename helper and find_incoming_calls rework
crates/perl-lsp-rs/src/call_hierarchy_provider/mod.rs
Adds a URI-basename helper and a synthetic file-level caller constructor, reworks named-subroutine traversal and FunctionCall/MethodCall matching to attribute top-level calls to a file caller, extends JSON kind mapping, and adds unit coverage.
Workspace-index caller synthesis and test
crates/perl-lsp-rs/src/runtime/language/hierarchy.rs
Changes the workspace-index incoming-call loop to synthesize a file-level caller when no enclosing callable is found, deduplicate by caller identity, and adds a workspace-only test for a top-level reference location.
Cross-file UX test activation
crates/perl-lsp-ux-tests/tests/ux_scenario_22_crossfile_extended_providers.rs
Enables the cross-file incomingCalls UX scenario and tightens its assertion to require a caller whose URI includes real-baseline.pl.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Suggested labels

size/M, testing

Poem

🐇 A call hopped out from every rootless tree,
A file stepped in and said, “That caller’s me!”
Baselines chirped, “Found at last, hooray!”
Top-level whispers now have names today.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing callHierarchy incomingCalls for top-level/script callers.
Description check ✅ Passed The description is directly related to the implemented fix and explains the affected paths and verification.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3093-incoming-top-level-caller

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements synthetic file-level callers for top-level call sites that are not enclosed in any named subroutine, preventing them from being silently dropped in incoming call hierarchies. It also enables and updates the corresponding integration test to verify this behavior. The review feedback suggests making the newly introduced uri_basename helper function pub(crate) and reusing it in hierarchy.rs to eliminate code duplication.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +53 to +55
fn uri_basename(uri: &str) -> String {
uri.rsplit('/').find(|s| !s.is_empty()).unwrap_or(uri).to_string()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The uri_basename helper function is currently private to this module, but the exact same logic is duplicated in hierarchy.rs. Making this function pub(crate) allows it to be reused across the crate, improving maintainability and reducing code duplication.

Suggested change
fn uri_basename(uri: &str) -> String {
uri.rsplit('/').find(|s| !s.is_empty()).unwrap_or(uri).to_string()
}
pub(crate) fn uri_basename(uri: &str) -> String {
uri.rsplit('/').find(|s| !s.is_empty()).unwrap_or(uri).to_string()
}

Comment on lines +640 to +645
let raw_uri = location.uri.as_str();
let basename = raw_uri
.rsplit('/')
.find(|s| !s.is_empty())
.unwrap_or(raw_uri)
.to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This block duplicates the URI basename extraction logic defined in call_hierarchy_provider/mod.rs. Reusing the uri_basename helper function here reduces code duplication and ensures consistent behavior.

                                let raw_uri = location.uri.as_str();
                                let basename = crate::call_hierarchy_provider::uri_basename(raw_uri);

@EffortlessSteven

Copy link
Copy Markdown
Member Author

Independent verification receipt

Orchestrator verified the durable artifact (not the builder self-report).

External-truth logic review — two seams the test cannot catch, both sound:

  • The return; added to the named-Subroutine arm is necessary and behavior-preserving: children of a named sub were visited twice (sub-context, then outer None-context); pre-fix the second pass was a no-op, post-fix it would have synthesized a spurious file-level caller for calls inside subs. The early return prevents exactly that.
  • No cross-path duplicate: the open-doc fallback feeds the same seen map keyed on (name, uri) as the workspace-index path, so a top-level site found by both paths merges (from_ranges extended) into one caller. Relies on location.uri == doc_uri, the same assumption the pre-existing named-sub dedup already depends on — no new bug.

Empirical (rebuilt + re-ran in the worktree, real exit codes):

  • cargo build -p perl-lsp-rs --bin perl-lsp → BUILD=0
  • cargo test -p perl-lsp-rs --lib call_hierarchy5 passed; 0 failed
  • whole ux_scenario_22_crossfile_extended_providers (incoming + outgoing + the rest), isolated with the real binary → 20 passed; 0 failed — confirms the un-ignored test passes AND the return; change regressed no other call-hierarchy case.

Step 0 confirmed: the top-level $app->run reference IS indexed (workspace_index.rs:4020 records all MethodCall method names unconditionally), so the synthetic-caller fix is at the right layer.

Residual / pending: the required CI gates (ripr+ New Gap Gate, Codecov/Patch 95) are the remaining unknowns — the new synthetic-caller branches add struct construction + branches that the static gates may flag; the hierarchy.rs workspace-index closure is integration-covered, not --lib. Marking ready to get their ground truth; will close any gate gap precisely from the CI artifacts.

@EffortlessSteven
EffortlessSteven marked this pull request as ready for review June 30, 2026 06:08
@factory-droid

factory-droid Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be37fc838b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +634 to +636
let from = self
.find_workspace_enclosing_callable(&callable_symbols, &location)
.unwrap_or_else(|| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter bare top-level refs before synthesizing callers

For top-level refs, this now turns every find_refs result without an enclosing callable into a file caller. In the default workspace path, WorkspaceIndex::find_refs for a qualified target such as App::run also returns bare run references, so an unrelated top-level Other::run()/run() in any indexed script reaches this branch and is reported as an incoming caller even though the package/receiver does not match. Please require an exact qualified match, or otherwise resolve the receiver, before synthesizing the file-level item.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perl-lsp-rs/src/runtime/language/hierarchy.rs`:
- Around line 633-667: The synthetic file-level callers created in the
incoming-calls path of hierarchy.rs are not being recognized when they
round-trip back through the request parser. Update json_to_call_hierarchy_item()
to map kind 1 to a file item, and add matching file-scoped handling wherever
call-hierarchy requests are resolved so these items don’t get treated as
function/subroutine callers; if that support is not intended yet, keep the
hierarchy builder from emitting reusable file items in the incomingCalls path.

In
`@crates/perl-lsp-ux-tests/tests/ux_scenario_22_crossfile_extended_providers.rs`:
- Around line 598-607: The current incomingCalls assertion in the crossfile
extended providers test only checks that a caller URI contains real-baseline.pl,
which can still pass for a named subroutine inside the same file. Tighten the
check in the test around script_caller so it asserts the synthesized file-level
caller contract by validating from.kind == 1 or from.name == "real-baseline.pl"
in addition to the URI, ensuring the App::run incoming call is attributed to the
top-level script file rather than any arbitrary caller in that file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f0b6a00-5da4-4a00-a899-5c27dd0c49ee

📥 Commits

Reviewing files that changed from the base of the PR and between 310ee2e and be37fc8.

📒 Files selected for processing (3)
  • crates/perl-lsp-rs/src/call_hierarchy_provider/mod.rs
  • crates/perl-lsp-rs/src/runtime/language/hierarchy.rs
  • crates/perl-lsp-ux-tests/tests/ux_scenario_22_crossfile_extended_providers.rs

Comment thread crates/perl-lsp-rs/src/runtime/language/hierarchy.rs
Comment on lines +598 to +607
// The caller must be the script file — not just any non-empty result.
// This guards against vacuous passes where an unrelated item happens to appear.
let script_caller = calls.iter().find(|c| {
c["from"]["uri"].as_str().map(|u| u.contains("real-baseline.pl")).unwrap_or(false)
});
assert!(
script_caller.is_some(),
"incomingCalls for `App::run` must include `real-baseline.pl` as a caller \
(top-level `$app->run` call). Got callers: {calls:?}"
);

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.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the synthesized file caller, not only the URI.

This still passes if the fixture later gains a named subroutine caller in the same script. Checking from.kind == 1 or from.name == "real-baseline.pl" would pin the file-level caller contract the PR is actually adding.

Suggested tightening
-    let script_caller = calls.iter().find(|c| {
-        c["from"]["uri"].as_str().map(|u| u.contains("real-baseline.pl")).unwrap_or(false)
-    });
+    let script_caller = calls.iter().find(|c| {
+        c["from"]["uri"].as_str().map(|u| u.ends_with("/real-baseline.pl")).unwrap_or(false)
+            && c["from"]["kind"].as_u64() == Some(1)
+            && c["from"]["name"].as_str() == Some("real-baseline.pl")
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// The caller must be the script file — not just any non-empty result.
// This guards against vacuous passes where an unrelated item happens to appear.
let script_caller = calls.iter().find(|c| {
c["from"]["uri"].as_str().map(|u| u.contains("real-baseline.pl")).unwrap_or(false)
});
assert!(
script_caller.is_some(),
"incomingCalls for `App::run` must include `real-baseline.pl` as a caller \
(top-level `$app->run` call). Got callers: {calls:?}"
);
// The caller must be the script file — not just any non-empty result.
// This guards against vacuous passes where an unrelated item happens to appear.
let script_caller = calls.iter().find(|c| {
c["from"]["uri"].as_str().map(|u| u.ends_with("/real-baseline.pl")).unwrap_or(false)
&& c["from"]["kind"].as_u64() == Some(1)
&& c["from"]["name"].as_str() == Some("real-baseline.pl")
});
assert!(
script_caller.is_some(),
"incomingCalls for `App::run` must include `real-baseline.pl` as a caller \
(top-level `$app->run` call). Got callers: {calls:?}"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@crates/perl-lsp-ux-tests/tests/ux_scenario_22_crossfile_extended_providers.rs`
around lines 598 - 607, The current incomingCalls assertion in the crossfile
extended providers test only checks that a caller URI contains real-baseline.pl,
which can still pass for a named subroutine inside the same file. Tighten the
check in the test around script_caller so it asserts the synthesized file-level
caller contract by validating from.kind == 1 or from.name == "real-baseline.pl"
in addition to the URI, ensuring the App::run incoming call is attributed to the
top-level script file rather than any arbitrary caller in that file.

@EffortlessSteven

Copy link
Copy Markdown
Member Author

Codecov/Patch 95: real failure behind a stale "pending"

The PR check still renders Codecov / Patch 95 — pending, but the underlying CI (Nightly) run completed with that job = failure (the check just hasn't reconciled — the false-pending trap). Coverage-proof artifact: patch coverage 19.72% / 95%, failure_class: coverage_shortfall.

Cause: the new synthetic-caller lines are exercised only by the integration test (ux_scenario_22), which does not count toward --lib patch coverage. Uncovered: call_hierarchy_provider/mod.rs:53-55,253-259 and runtime/language/hierarchy.rs:633-645.

Remediation in progress (no logic change): extract a shared synthetic_file_level_caller helper and add --lib unit tests for the open-doc synthesis (modeled on test_incoming_calls) and the workspace-index path (modeled on test_wait_guard_fires_in_incoming_calls... with a Full index). The fix is correct and verified (20/20 scenario_22, 5/5 unit) — this is purely closing the --lib patch-coverage gap. New commits incoming; CI re-runs on push.

…lib tests (#3093)

Extract the inline file-level CallHierarchyItem construction into a
`pub(crate) fn synthetic_file_level_caller(uri, range)` helper in
`call_hierarchy_provider/mod.rs`.  Both the open-document traversal path
(FunctionCall / MethodCall arms) and the workspace-index path (hierarchy.rs
unwrap_or_else closure) now call the helper, concentrating synthesis logic
in one --lib-testable location.

Add three --lib unit tests in mod.rs:
  - test_synthetic_file_level_caller_returns_file_item (direct helper test)
  - test_incoming_calls_top_level_method_call_synthesizes_file_caller
  - test_incoming_calls_top_level_function_call_synthesizes_file_caller

Add one --lib unit test in hierarchy.rs:
  - test_incoming_calls_workspace_path_synthesizes_file_level_caller
    (uses Full workspace index with App->run() at top level of a script
    to cover the unwrap_or_else closure + seen-map insert, lines 633-645)

Fixes Codecov/Patch-95 coverage gap on PR #3191.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@factory-droid

factory-droid Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EffortlessSteven's task —— View job


Phase 2 (validator) — review validated

PR #3191 (fix/3093-incoming-top-level-caller @ cec64d1d8)

Read full diff, validated both candidate comments against current source, posted both as a single batched review.

Results: 2 approved, 0 rejected

  1. P1 Workspace-index path mis-attributes bare-name cross-package matches. find_refs for non-main packages routes through find_references(qualified_name), which the dual-indexing pattern also walks to gather bare-name matches — so a top-level $other->run in the same script ends up attributed to App::run. Posted on the unwrap_or_else closure at lines 635-643 (NEW file).
  2. P1 Synthesized file-kind callers round-trip fails. json_to_call_hierarchy_item (lines 806-810) only maps 6 => "method" and defaults 1 to "function", so a client navigating back from a file-kind caller silently gets empty results. The json_to_call_hierarchy_item body is unchanged and falls outside any diff hunk's commentable window, so the comment was anchored on the synthesis site (lines 640-644) and names the deserializer lines in the body.

Review summary: both candidates identify real defects in the synthetic file-level caller contract that are not exercised by the new test (which only tests a single-package top-level call). Fix-forward on the builder side will need a qualified-key filter in unwrap_or_else and a 1 => "file" arm in json_to_call_hierarchy_item.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perl-lsp-rs/src/runtime/language/hierarchy.rs`:
- Line 980: Replace the `map_or(false, ...)` predicate in the `find` call within
`hierarchy.rs` with `is_some_and` on the `from.uri` lookup to satisfy clippy’s
`unnecessary_map_or` lint. Keep the existing `find` logic and `script.pl`
containment check, but update the `as_str()` handling to use `is_some_and` so
the `from`/`uri` access remains concise and warning-free.
- Around line 940-988: The test in hierarchy.rs introduces new `.expect()` calls
that will fail the `clippy::expect_used` gate. Update the
`handle_incoming_calls` assertion flow to avoid `expect` by using `assert!`,
`match`, or `ok_or_else` around the `result`, `value`, `calls`, and
`file_caller` checks, while keeping the same verification logic in the
incoming-calls test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80715f96-15d9-46be-856d-8d6bbe53764f

📥 Commits

Reviewing files that changed from the base of the PR and between be37fc8 and cec64d1.

📒 Files selected for processing (2)
  • crates/perl-lsp-rs/src/call_hierarchy_provider/mod.rs
  • crates/perl-lsp-rs/src/runtime/language/hierarchy.rs

Comment on lines +940 to +988
server
.test_index_file_in_building_state(script_uri, script_text)
.expect("indexing script.pl");
// Transition coordinator to Ready so workspace path is taken.
server.test_simulate_indexing_complete();

// Also open as a document so open-doc fallback doesn't add duplicates.
open_doc(&server, script_uri, script_text);

// incomingCalls for "App::run" — data.packageName drives workspace_symbol_key.
let result = server.handle_incoming_calls(Some(json!({
"item": {
"name": "run",
"kind": 6,
"uri": "file:///App.pm",
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 2, "character": 1 }
},
"selectionRange": {
"start": { "line": 1, "character": 4 },
"end": { "line": 1, "character": 7 }
},
"data": {
"packageName": "App",
"qualifiedName": "App::run"
}
}
})));

assert!(result.is_ok(), "handle_incoming_calls must not error: {result:?}");
let value = result.expect("already checked");
let value = value.expect("handler must return Some value");
// handle_incoming_calls returns the calls array directly (not wrapped in {"result":...})
let calls = value.as_array().expect("result should be an array");

// The reference in script.pl has no enclosing callable, so the workspace
// path must synthesize a file-level caller with kind=1 (SymbolKind.File).
let file_caller = calls
.iter()
.find(|c| c["from"]["uri"].as_str().map_or(false, |u| u.contains("script.pl")));
assert!(file_caller.is_some(), "expected file-level caller from script.pl, got: {calls:?}");
let from = &file_caller.expect("already checked")["from"];
assert_eq!(
from["kind"].as_u64(),
Some(1),
"file-level caller must have SymbolKind.File=1, got: {from:?}"
);
assert_eq!(from["name"].as_str(), Some("script.pl"));

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'expect_used|deny\(|warn\(|clippy' --type=toml
fd -t f 'clippy.toml|.clippy.toml'
rg -nP '#!\[(deny|warn)\(clippy::expect_used' -g '*.rs'

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## clippy config and expect_used overrides\n'
git ls-files | rg '(^|/)(clippy\.toml|\.clippy\.toml|Cargo\.toml)$|\.rs$' >/dev/null 2>&1 || true
rg -n --hidden --glob '!**/target/**' --glob '!**/node_modules/**' \
  'expect_used|clippy::expect_used|allow\(clippy::expect_used\)|deny\(clippy::expect_used\)|warn\(clippy::expect_used\)|clippy::all|clippy::pedantic|clippy::restriction' \
  . || true

printf '\n## target file excerpt around the reviewed lines\n'
sed -n '930,995p' crates/perl-lsp-rs/src/runtime/language/hierarchy.rs

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 19144


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## files"
fd -H -t f 'clippy.toml|.clippy.toml|Cargo.toml' .

echo
echo "## clippy/expect_used matches"
rg -n --hidden --glob '!**/target/**' --glob '!**/node_modules/**' \
  'expect_used|clippy::expect_used|allow\(clippy::expect_used\)|deny\(clippy::expect_used\)|warn\(clippy::expect_used\)|clippy::all|clippy::pedantic|clippy::restriction' \
  .

echo
echo "## hierarchy.rs excerpt"
sed -n '930,995p' crates/perl-lsp-rs/src/runtime/language/hierarchy.rs

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 20544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## crate root for perl-lsp-rs"
sed -n '1,120p' crates/perl-lsp-rs/src/lib.rs

echo
echo "## any test-only clippy allowances in perl-lsp-rs"
rg -n --hidden --glob '!**/target/**' \
  'cfg_attr\(test, allow\(.*expect_used|allow\(clippy::expect_used|allow\(clippy::unwrap_used|allow\(clippy::panic' \
  crates/perl-lsp-rs/src crates/perl-lsp-rs/tests crates/perl-lsp-rs/Cargo.toml

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 4582


Replace the new .expect() calls in this test. Cargo.toml denies clippy::expect_used, and there’s no local test allowance here, so these uses will trip the clippy gate. Use assert!/match/ok_or_else instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perl-lsp-rs/src/runtime/language/hierarchy.rs` around lines 940 - 988,
The test in hierarchy.rs introduces new `.expect()` calls that will fail the
`clippy::expect_used` gate. Update the `handle_incoming_calls` assertion flow to
avoid `expect` by using `assert!`, `match`, or `ok_or_else` around the `result`,
`value`, `calls`, and `file_caller` checks, while keeping the same verification
logic in the incoming-calls test.

Source: Coding guidelines

// path must synthesize a file-level caller with kind=1 (SymbolKind.File).
let file_caller = calls
.iter()
.find(|c| c["from"]["uri"].as_str().map_or(false, |u| u.contains("script.pl")));

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

clippy unnecessary_map_or lint version stabilized

💡 Result:

The Clippy lint unnecessary_map_or was merged into the master branch of the Rust Clippy repository on November 13, 2024 [1]. It was introduced to simplify map_or method calls where a more idiomatic or readable alternative exists [1]. Because Clippy lints are bundled with the Rust toolchain and released periodically alongside rustc, the lint became available to users in the stable Rust releases following its merge in late 2024. It is part of the standard set of Clippy lints and is actively maintained, with ongoing refinements and discussions regarding its behavior and potential future deprecation or splitting into smaller, more specialized lints [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== hierarchy.rs around line 980 ==\n'
sed -n '950,1010p' crates/perl-lsp-rs/src/runtime/language/hierarchy.rs | cat -n

printf '\n== clippy / warnings config ==\n'
rg -n --hidden --glob '!target' --glob '!node_modules' 'deny\s*\(\s*warnings\s*\)|unnecessary_map_or|clippy::|warnings' Cargo.toml crates/perl-lsp-rs crates -g 'Cargo.toml' -g '*.rs' | sed -n '1,200p'

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 22017


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- hierarchy.rs slice ---'
sed -n '970,990p' crates/perl-lsp-rs/src/runtime/language/hierarchy.rs | cat -n

echo
echo '--- lint-related config ---'
rg -n --hidden --glob '!target' --glob '!node_modules' \
  'deny\s*\(\s*warnings\s*\)|#!\s*\[\s*deny\s*\(\s*warnings\s*\)\s*\]|clippy::|warnings as errors|deny-warnings|-D warnings' \
  Cargo.toml .cargo crates/perl-lsp-rs crates -g 'Cargo.toml' -g '*.rs' -g '*.toml' | sed -n '1,200p'

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 17141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workflow / CI references ==\n'
rg -n --hidden --glob '!target' --glob '!node_modules' \
  'agent-clippy|cargo clippy|clippy --workspace|-\s*D warnings|D warnings|profile agent' \
  .github Cargo.toml .cargo crates -g '*.yml' -g '*.yaml' -g '*.toml' -g '*.md' -g '*.rs' | sed -n '1,240p'

Repository: EffortlessMetrics/perl-lsp-swarm

Length of output: 5128


Use is_some_and here. map_or(false, ...) trips clippy::unnecessary_map_or, and clippy runs with -D warnings in this workspace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perl-lsp-rs/src/runtime/language/hierarchy.rs` at line 980, Replace
the `map_or(false, ...)` predicate in the `find` call within `hierarchy.rs` with
`is_some_and` on the `from.uri` lookup to satisfy clippy’s `unnecessary_map_or`
lint. Keep the existing `find` logic and `script.pl` containment check, but
update the `as_str()` handling to use `is_some_and` so the `from`/`uri` access
remains concise and warning-free.

Comment on lines +635 to +643
.find_workspace_enclosing_callable(&callable_symbols, &location)
.unwrap_or_else(|| {
// Top-level call site — no enclosing callable in the
// workspace index. Synthesize a file-level caller so the
// script appears in incomingCalls instead of being dropped.
crate::call_hierarchy_provider::synthetic_file_level_caller(
&location.uri,
from_range,
)

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.

[P1] Workspace-index path synthesizes file-level callers for unrelated bare-name matches

find_refs(SymbolKey{pkg:"App", name:"run"}) returns BOTH qualified App::run matches AND bare run matches (see find_references at workspace_index.rs:2211, which also looks up the bare suffix). The new unwrap_or_else synthesizes a file-level caller for every returned location without verifying that the bare reference actually invokes the qualified target. Concrete trigger: a workspace containing script.pl with top-level $app->run AND $other->run will produce a single file-level caller for script.pl whose from_ranges contains a location that calls Other::run, not App::run — the caller is mis-attributed. The pre-fix code silently dropped these (no enclosing sub + if let Some(from) guard), so this PR converts a silent drop into a wrong positive. Fix: filter by qualified name before synthesizing, e.g. only synthesize a file-level caller when the workspace-index recorded the reference under the qualified key for this target.

Comment on lines +640 to +644
crate::call_hierarchy_provider::synthetic_file_level_caller(
&location.uri,
from_range,
)
});

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.

[P1] File-kind synthesized callers here do not round-trip through json_to_call_hierarchy_item (lines 806-810)

The synthetic file-level callers produced by the unwrap_or_else above serialize to kind: 1 (SymbolKind.File) via to_json. But json_to_call_hierarchy_item in this file only maps 6 => "method" and defaults everything else (including 1) to "function". If a client later sends one of these file-kind callers back in callHierarchy/incomingCalls or callHierarchy/outgoingCalls to expand it, the server will treat it as a function-kind item whose name = "real-baseline.pl" and search for a subroutine named after the filename — silently returning empty results. Fix: extend the kind match in the deserializer with 1 => "file" so file-kind items round-trip cleanly.

Suggested change
crate::call_hierarchy_provider::synthetic_file_level_caller(
&location.uri,
from_range,
)
});
let kind = match json["kind"].as_u64().unwrap_or(12) {
1 => "file",
6 => "method",
_ => "function",
}
.to_string();

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.64706% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...tes/perl-lsp-rs/src/call_hierarchy_provider/mod.rs 97.24% 3 Missing ⚠️
...ates/perl-lsp-rs/src/runtime/language/hierarchy.rs 98.36% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@EffortlessSteven
EffortlessSteven merged commit d91b40a into main Jun 30, 2026
75 checks passed
@EffortlessSteven
EffortlessSteven deleted the fix/3093-incoming-top-level-caller branch June 30, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant