-
Notifications
You must be signed in to change notification settings - Fork 1
fix(lsp): callHierarchy/incomingCalls returns top-level/script callers (#3093) #3191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -630,22 +630,29 @@ impl LspServer { | |||||||||||||||||||||||
| let refs = index.find_refs(&symbol_key); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for location in refs { | ||||||||||||||||||||||||
| if let Some(from) = | ||||||||||||||||||||||||
| self.find_workspace_enclosing_callable(&callable_symbols, &location) | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| let key = (from.name.clone(), from.uri.clone()); | ||||||||||||||||||||||||
| let from_range = index_location_to_wire_range(&location); | ||||||||||||||||||||||||
| if let Some(&idx) = seen.get(&key) { | ||||||||||||||||||||||||
| all_calls[idx].from_ranges.push(from_range); | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| seen.insert(key, all_calls.len()); | ||||||||||||||||||||||||
| all_calls.push( | ||||||||||||||||||||||||
| crate::call_hierarchy_provider::CallHierarchyIncomingCall { | ||||||||||||||||||||||||
| from, | ||||||||||||||||||||||||
| from_ranges: vec![from_range], | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| let from_range = index_location_to_wire_range(&location); | ||||||||||||||||||||||||
| let from = self | ||||||||||||||||||||||||
| .find_workspace_enclosing_callable(&callable_symbols, &location) | ||||||||||||||||||||||||
| .unwrap_or_else(|| { | ||||||||||||||||||||||||
|
Comment on lines
+634
to
+636
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For top-level refs, this now turns every Useful? React with 👍 / 👎. |
||||||||||||||||||||||||
| // 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, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+635
to
+643
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
Comment on lines
+640
to
+644
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] File-kind synthesized callers here do not round-trip through The synthetic file-level callers produced by the
Suggested change
|
||||||||||||||||||||||||
| let key = (from.name.clone(), from.uri.clone()); | ||||||||||||||||||||||||
| if let Some(&idx) = seen.get(&key) { | ||||||||||||||||||||||||
| all_calls[idx].from_ranges.push(from_range); | ||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||
| seen.insert(key, all_calls.len()); | ||||||||||||||||||||||||
| all_calls.push( | ||||||||||||||||||||||||
| crate::call_hierarchy_provider::CallHierarchyIncomingCall { | ||||||||||||||||||||||||
| from, | ||||||||||||||||||||||||
| from_ranges: vec![from_range], | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
@@ -912,6 +919,75 @@ mod tests { | |||||||||||||||||||||||
| assert!(result.is_ok(), "handle_incoming_calls must not error: {result:?}"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Verifies that the workspace-index path in `handle_incoming_calls` synthesizes | ||||||||||||||||||||||||
| /// a file-level `CallHierarchyItem` (kind=1/File) when a reference location in | ||||||||||||||||||||||||
| /// the index has no enclosing callable symbol — i.e., it is a top-level call. | ||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||
| /// Covers lines 633-645 (the `unwrap_or_else` closure + seen-map insert) for the | ||||||||||||||||||||||||
| /// Codecov/Patch-95 gate (#3093). | ||||||||||||||||||||||||
| #[cfg(feature = "workspace")] | ||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||
| fn test_incoming_calls_workspace_path_synthesizes_file_level_caller() { | ||||||||||||||||||||||||
| let server = LspServer::new(); | ||||||||||||||||||||||||
| server.test_enable_call_hierarchy(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // script.pl: static method call at the TOP LEVEL (no enclosing sub). | ||||||||||||||||||||||||
| // App->run() is a static call so workspace_index stores it as "App::run". | ||||||||||||||||||||||||
| let script_uri = "file:///script.pl"; | ||||||||||||||||||||||||
| let script_text = "App->run();\n"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Index the file (transitions coordinator to Building internally). | ||||||||||||||||||||||||
| 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"))); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: The Clippy lint 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 🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| 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")); | ||||||||||||||||||||||||
|
Comment on lines
+940
to
+988
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.rsRepository: 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.rsRepository: 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.tomlRepository: EffortlessMetrics/perl-lsp-swarm Length of output: 4582 Replace the new 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Verifies that `handle_outgoing_calls` executes the workspace | ||||||||||||||||||||||||
| /// index-readiness wait when indexing is in progress (#3095). | ||||||||||||||||||||||||
| #[cfg(feature = "workspace")] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -538,12 +538,10 @@ fn scenario_22_call_hierarchy_outgoing_from_run_hard_assert() -> anyhow::Result< | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /// Hard assert — callHierarchy/incomingCalls for `run` must return at least one caller. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||
| /// BROKEN on current main: incomingCalls returns empty [] even though | ||||||||||||||||||||||||||||||||||||||||||||||
| /// script/real-baseline.pl calls `$app->run`. OO arrow-method caller lookup | ||||||||||||||||||||||||||||||||||||||||||||||
| /// is not resolving back to the CallHierarchyItem. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// Tracking: #3093 | ||||||||||||||||||||||||||||||||||||||||||||||
| /// Fixed in #3093: top-level callers (not inside any `sub`) are now returned as | ||||||||||||||||||||||||||||||||||||||||||||||
| /// file-level CallHierarchyItems instead of being silently dropped. | ||||||||||||||||||||||||||||||||||||||||||||||
| /// script/real-baseline.pl calls `$app->run` at the top level — must appear. | ||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| #[ignore = "real gap — incomingCalls returns empty for OO method callers; tracking #3093"] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn scenario_22_call_hierarchy_incoming_to_run_hard_assert() -> anyhow::Result<()> { | ||||||||||||||||||||||||||||||||||||||||||||||
| if !binary_available() { | ||||||||||||||||||||||||||||||||||||||||||||||
| eprintln!("SKIP scenario_22: perl-lsp binary not found"); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -597,6 +595,17 @@ fn scenario_22_call_hierarchy_incoming_to_run_hard_assert() -> anyhow::Result<() | |||||||||||||||||||||||||||||||||||||||||||||
| script/real-baseline.pl calls `$app->run`. Got: []" | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // 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:?}" | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+598
to
+607
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| harness.assert_no_crash(); | ||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
uri_basenamehelper function is currently private to this module, but the exact same logic is duplicated inhierarchy.rs. Making this functionpub(crate)allows it to be reused across the crate, improving maintainability and reducing code duplication.