Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 146 additions & 26 deletions crates/perl-lsp-rs/src/call_hierarchy_provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ pub struct CallHierarchyProvider {
position_mapper: PositionMapper,
}

/// Extract the filename from a URI (e.g. `"file:///path/to/foo.pl"` → `"foo.pl"`).
///
/// Used to name synthetic file-level callers for top-level call sites that are
/// not enclosed in any named subroutine.
fn uri_basename(uri: &str) -> String {
uri.rsplit('/').find(|s| !s.is_empty()).unwrap_or(uri).to_string()
}
Comment on lines +53 to +55

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()
}


/// Synthesize a `CallHierarchyItem` representing a file-level (top-level) caller.
///
/// Used when a call site is not enclosed in any named subroutine — the script
/// file itself becomes the logical "caller" so it appears in `incomingCalls`
/// instead of being silently dropped.
///
/// Both the open-document traversal path (`mod.rs`) and the workspace-index
/// path (`hierarchy.rs`) use this helper, keeping synthesis logic in one
/// `--lib`-testable location.
pub(crate) fn synthetic_file_level_caller(uri: &str, range: Range) -> CallHierarchyItem {
CallHierarchyItem {
name: uri_basename(uri),
kind: "file".to_string(),
uri: uri.to_string(),
range,
selection_range: range,
detail: None,
package_name: None,
qualified_name: None,
}
}

impl CallHierarchyProvider {
/// Create a new call hierarchy provider for a source file
///
Expand Down Expand Up @@ -222,47 +252,49 @@ impl CallHierarchyProvider {
qualified_name: None,
};

// Search within this function
// Search within this named function; return early so the
// bottom visitor does not re-visit children with the outer
// (possibly None) context, which would create spurious
// file-level callers for calls inside this sub.
self.visit_children(node, |child| {
self.find_incoming_calls(child, target_name, calls, Some(&item));
None::<()>
});
return;
}
// Anonymous sub — fall through to the bottom visitor.
}
NodeKind::FunctionCall { name, .. } => {
// Match exact name or package-qualified name (e.g. "Utils::format_string")
let matches = name == target_name || name.ends_with(&format!("::{}", target_name));
if matches {
if let Some(from) = current_function {
let ranges = vec![self.node_to_range(node)];

// Check if we already have a call from this function
if let Some(existing) = calls.iter_mut().find(|c| c.from.name == from.name)
{
existing.from_ranges.extend(ranges);
} else {
calls.push(CallHierarchyIncomingCall {
from: from.clone(),
from_ranges: ranges,
});
}
let call_range = self.node_to_range(node);
let from = current_function.cloned().unwrap_or_else(|| {
// Top-level call site — synthesize a file-level caller so the
// script appears in incomingCalls instead of being silently dropped.
synthetic_file_level_caller(uri, call_range)
});
let ranges = vec![call_range];
if let Some(existing) = calls.iter_mut().find(|c| c.from.name == from.name) {
existing.from_ranges.extend(ranges);
} else {
calls.push(CallHierarchyIncomingCall { from, from_ranges: ranges });
}
}
}
NodeKind::MethodCall { method, .. } => {
if method == target_name {
if let Some(from) = current_function {
let ranges = vec![self.node_to_range(node)];

if let Some(existing) = calls.iter_mut().find(|c| c.from.name == from.name)
{
existing.from_ranges.extend(ranges);
} else {
calls.push(CallHierarchyIncomingCall {
from: from.clone(),
from_ranges: ranges,
});
}
let call_range = self.node_to_range(node);
let from = current_function.cloned().unwrap_or_else(|| {
// Top-level call site — synthesize a file-level caller so the
// script appears in incomingCalls instead of being silently dropped.
synthetic_file_level_caller(uri, call_range)
});
let ranges = vec![call_range];
if let Some(existing) = calls.iter_mut().find(|c| c.from.name == from.name) {
existing.from_ranges.extend(ranges);
} else {
calls.push(CallHierarchyIncomingCall { from, from_ranges: ranges });
}
}
}
Expand Down Expand Up @@ -656,6 +688,7 @@ impl CallHierarchyItem {
"kind": match self.kind.as_str() {
"function" => 12, // SymbolKind.Function
"method" => 6, // SymbolKind.Method
"file" => 1, // SymbolKind.File
_ => 12,
},
"uri": self.uri,
Expand Down Expand Up @@ -916,4 +949,91 @@ sub helper {
assert!(called_names.contains(&&"method_call".to_string()));
}
}

/// `synthetic_file_level_caller` must return a `CallHierarchyItem` with kind
/// `"file"`, the basename of the URI as the name, and both `range` /
/// `selection_range` set to the supplied range.
#[test]
fn test_synthetic_file_level_caller_returns_file_item() {
let range = Range {
start: Position { line: 5, character: 0 },
end: Position { line: 5, character: 20 },
};
let item = synthetic_file_level_caller("file:///path/to/script.pl", range);
assert_eq!(item.name, "script.pl");
assert_eq!(item.kind, "file");
assert_eq!(item.uri, "file:///path/to/script.pl");
assert_eq!(item.range.start.line, 5);
assert_eq!(item.range.end.character, 20);
assert_eq!(item.selection_range.start.line, 5);
assert!(item.detail.is_none());
assert!(item.package_name.is_none());
assert!(item.qualified_name.is_none());
}

/// A top-level `MethodCall` (not inside any sub) must produce a file-level
/// caller rather than being silently dropped.
#[test]
fn test_incoming_calls_top_level_method_call_synthesizes_file_caller() {
let code = "App->run();\n";
let mut parser = Parser::new(code);
if let Ok(ast) = parser.parse() {
let provider =
CallHierarchyProvider::new(code.to_string(), "file:///script.pl".to_string());
let target_item = CallHierarchyItem {
name: "run".to_string(),
kind: "method".to_string(),
uri: "file:///App.pm".to_string(),
range: Range {
start: Position { line: 0, character: 0 },
end: Position { line: 2, character: 1 },
},
selection_range: Range {
start: Position { line: 1, character: 4 },
end: Position { line: 1, character: 7 },
},
detail: None,
package_name: None,
qualified_name: None,
};
let incoming = provider.incoming_calls(&ast, &target_item);
assert_eq!(incoming.len(), 1, "expected exactly one file-level caller");
assert_eq!(incoming[0].from.name, "script.pl");
assert_eq!(incoming[0].from.kind, "file");
assert_eq!(incoming[0].from.uri, "file:///script.pl");
}
}

/// A top-level `FunctionCall` (not inside any sub) must produce a file-level
/// caller rather than being silently dropped.
#[test]
fn test_incoming_calls_top_level_function_call_synthesizes_file_caller() {
let code = "target_func();\n";
let mut parser = Parser::new(code);
if let Ok(ast) = parser.parse() {
let provider =
CallHierarchyProvider::new(code.to_string(), "file:///script.pl".to_string());
let target_item = CallHierarchyItem {
name: "target_func".to_string(),
kind: "function".to_string(),
uri: "file:///lib.pm".to_string(),
range: Range {
start: Position { line: 0, character: 0 },
end: Position { line: 2, character: 1 },
},
selection_range: Range {
start: Position { line: 0, character: 4 },
end: Position { line: 0, character: 15 },
},
detail: None,
package_name: None,
qualified_name: None,
};
let incoming = provider.incoming_calls(&ast, &target_item);
assert_eq!(incoming.len(), 1, "expected exactly one file-level caller");
assert_eq!(incoming[0].from.name, "script.pl");
assert_eq!(incoming[0].from.kind, "file");
assert_eq!(incoming[0].from.uri, "file:///script.pl");
}
}
}
108 changes: 92 additions & 16 deletions crates/perl-lsp-rs/src/runtime/language/hierarchy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

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 👍 / 👎.

// 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

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

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();

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],
},
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -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")));

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.

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

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

}

/// Verifies that `handle_outgoing_calls` executes the workspace
/// index-readiness wait when indexing is in progress (#3095).
#[cfg(feature = "workspace")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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

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.


harness.assert_no_crash();
Ok(())
}
Expand Down
Loading