Skip to content

Commit bb8cb82

Browse files
committed
Deduplicate scope map helpers for extract-function, extract-variable,
and inline-variable
1 parent 461481e commit bb8cb82

5 files changed

Lines changed: 143 additions & 280 deletions

File tree

docs/todo/refactor.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,17 +223,17 @@ already tracks this information during its top-to-bottom pass.
223223
### `extract_closure_return_type_from_assignment`
224224

225225
Uses `rfind("$fn = ")` backward from cursor, then parses closure return type
226-
from raw text. The forward walker already processes closure assignments and
227-
extracts return types from the AST node's type hint. Callers in
228-
`call_resolution.rs` (L1272) and `rhs_resolution.rs` (L2695) should read the
229-
variable's type from the scope map instead.
226+
from raw text. The forward walker does NOT currently store callable return type
227+
info — it stores the variable as plain `Closure` via `resolve_rhs_expression`.
228+
Eliminating this backward walker requires teaching `resolve_rhs_expression` (or
229+
`resolve_rhs_with_scope`) to produce a `Callable(params, return_type)` PhpType
230+
for closure/arrow-function expressions.
230231

231232
### `extract_first_class_callable_return_type`
232233

233234
Uses `rfind("$fn = ")` backward, then resolves `Foo::bar(...)` callable return
234-
type from text. The forward walker already handles first-class callable
235-
assignments. Callers in `call_resolution.rs` (L1292) and `rhs_resolution.rs`
236-
(L2718) should use the scope map.
235+
type from text. Same situation: the forward walker stores plain `Closure` for
236+
first-class callable assignments. Needs the same `Callable` type support.
237237

238238
### `extract_function_return_from_source`
239239

@@ -252,8 +252,11 @@ re-parse the AST to extract information the SymbolMap already has.
252252

253253
### Scope boundary detection
254254

255-
The SymbolMap stores `scopes` (function/closure boundaries) and provides
256-
`find_enclosing_scope()`. Yet several code action helpers and diagnostic modules
257-
manually walk the AST to find the enclosing function/method body (e.g.
258-
`build_scope_map` in extract variable, inline variable, undefined variable
259-
diagnostics).
255+
The three `build_scope_map` helpers in extract-function, extract-variable, and
256+
inline-variable have been unified into a single
257+
`scope_collector::build_scope_map_for_offset` function. These code actions still
258+
re-parse the AST (via `with_parsed_program`, which caches) rather than using the
259+
SymbolMap's `find_enclosing_scope()`, because they need the full `ScopeMap`
260+
(variable reads/writes/frames) which the SymbolMap does not provide. Further
261+
consolidation would require either enriching the SymbolMap with variable
262+
access tracking or caching the `ScopeMap` per file.

src/code_actions/extract_function.rs

Lines changed: 6 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ use crate::code_actions::{CodeActionData, make_code_action_data};
2222
use crate::completion::phpdoc::generation::enrichment_plain;
2323
use crate::completion::resolver::Loaders;
2424
use crate::php_type::PhpType;
25-
use crate::scope_collector::{
26-
FrameKind, ScopeMap, collect_function_scope, collect_function_scope_with_kind, collect_scope,
27-
};
25+
use crate::scope_collector::ScopeMap;
2826
use crate::types::ClassInfo;
2927
use crate::util::{find_class_at_offset, offset_to_position, position_to_byte_offset};
3028

@@ -312,100 +310,11 @@ fn build_scope_map(content: &str, offset: u32) -> ScopeMap {
312310
let arena = Bump::new();
313311
let file_id = mago_database::file::FileId::new("extract_fn_scope");
314312
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
315-
316-
for stmt in program.statements.iter() {
317-
if let Some(map) = try_build_scope_from_statement(stmt, offset) {
318-
return map;
319-
}
320-
}
321-
322-
// Fallback: top-level scope.
323-
let body_end = content.len() as u32;
324-
collect_scope(program.statements.as_slice(), 0, body_end)
325-
}
326-
327-
/// Recursively try to build a scope map from a statement.
328-
fn try_build_scope_from_statement(stmt: &Statement<'_>, offset: u32) -> Option<ScopeMap> {
329-
match stmt {
330-
Statement::Function(func) => {
331-
let body_start = func.body.left_brace.start.offset;
332-
let body_end = func.body.right_brace.end.offset;
333-
if offset >= body_start && offset <= body_end {
334-
return Some(collect_function_scope(
335-
&func.parameter_list,
336-
func.body.statements.as_slice(),
337-
body_start,
338-
body_end,
339-
));
340-
}
341-
}
342-
Statement::Class(class) => {
343-
for member in class.members.iter() {
344-
if let ClassLikeMember::Method(method) = member
345-
&& let MethodBody::Concrete(block) = &method.body
346-
{
347-
let body_start = block.left_brace.start.offset;
348-
let body_end = block.right_brace.end.offset;
349-
if offset >= body_start && offset <= body_end {
350-
return Some(collect_function_scope_with_kind(
351-
&method.parameter_list,
352-
block.statements.as_slice(),
353-
body_start,
354-
body_end,
355-
FrameKind::Method,
356-
));
357-
}
358-
}
359-
}
360-
}
361-
Statement::Trait(tr) => {
362-
for member in tr.members.iter() {
363-
if let ClassLikeMember::Method(method) = member
364-
&& let MethodBody::Concrete(block) = &method.body
365-
{
366-
let body_start = block.left_brace.start.offset;
367-
let body_end = block.right_brace.end.offset;
368-
if offset >= body_start && offset <= body_end {
369-
return Some(collect_function_scope_with_kind(
370-
&method.parameter_list,
371-
block.statements.as_slice(),
372-
body_start,
373-
body_end,
374-
FrameKind::Method,
375-
));
376-
}
377-
}
378-
}
379-
}
380-
Statement::Enum(en) => {
381-
for member in en.members.iter() {
382-
if let ClassLikeMember::Method(method) = member
383-
&& let MethodBody::Concrete(block) = &method.body
384-
{
385-
let body_start = block.left_brace.start.offset;
386-
let body_end = block.right_brace.end.offset;
387-
if offset >= body_start && offset <= body_end {
388-
return Some(collect_function_scope_with_kind(
389-
&method.parameter_list,
390-
block.statements.as_slice(),
391-
body_start,
392-
body_end,
393-
FrameKind::Method,
394-
));
395-
}
396-
}
397-
}
398-
}
399-
Statement::Namespace(ns) => {
400-
for inner in ns.statements().iter() {
401-
if let Some(map) = try_build_scope_from_statement(inner, offset) {
402-
return Some(map);
403-
}
404-
}
405-
}
406-
_ => {}
407-
}
408-
None
313+
crate::scope_collector::build_scope_map_for_offset(
314+
program.statements.as_slice(),
315+
offset,
316+
content.len() as u32,
317+
)
409318
}
410319

411320
// ─── Type resolution ────────────────────────────────────────────────────────

src/code_actions/extract_variable.rs

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tower_lsp::lsp_types::*;
1212
use crate::Backend;
1313
use crate::code_actions::{CodeActionData, make_code_action_data};
1414
use crate::parser::with_parsed_program;
15-
use crate::scope_collector::{ScopeMap, collect_function_scope, collect_scope};
15+
use crate::scope_collector::ScopeMap;
1616
use crate::util::{find_identical_occurrences, offset_to_position, position_to_byte_offset};
1717

1818
// ─── Name generation ────────────────────────────────────────────────────────
@@ -583,80 +583,12 @@ fn find_enclosing_statement_line(content: &str, selection_start: usize) -> (usiz
583583
/// This finds the enclosing function/method scope or falls back to
584584
/// top-level scope.
585585
fn build_scope_map(content: &str, offset: u32) -> ScopeMap {
586-
use mago_syntax::ast::*;
587-
588-
with_parsed_program(content, "extract_variable", |program, _content| {
589-
// Try to find the enclosing function or method.
590-
for stmt in program.statements.iter() {
591-
if let Statement::Function(func) = stmt {
592-
let body_start = func.body.left_brace.start.offset;
593-
let body_end = func.body.right_brace.end.offset;
594-
if offset >= body_start && offset <= body_end {
595-
return collect_function_scope(
596-
&func.parameter_list,
597-
func.body.statements.as_slice(),
598-
body_start,
599-
body_end,
600-
);
601-
}
602-
}
603-
if let Statement::Class(class) = stmt {
604-
for member in class.members.iter() {
605-
if let ClassLikeMember::Method(method) = member
606-
&& let MethodBody::Concrete(block) = &method.body
607-
{
608-
let body_start = block.left_brace.start.offset;
609-
let body_end = block.right_brace.end.offset;
610-
if offset >= body_start && offset <= body_end {
611-
return collect_function_scope(
612-
&method.parameter_list,
613-
block.statements.as_slice(),
614-
body_start,
615-
body_end,
616-
);
617-
}
618-
}
619-
}
620-
}
621-
if let Statement::Namespace(ns) = stmt {
622-
for inner_stmt in ns.statements().iter() {
623-
if let Statement::Function(func) = inner_stmt {
624-
let body_start = func.body.left_brace.start.offset;
625-
let body_end = func.body.right_brace.end.offset;
626-
if offset >= body_start && offset <= body_end {
627-
return collect_function_scope(
628-
&func.parameter_list,
629-
func.body.statements.as_slice(),
630-
body_start,
631-
body_end,
632-
);
633-
}
634-
}
635-
if let Statement::Class(class) = inner_stmt {
636-
for member in class.members.iter() {
637-
if let ClassLikeMember::Method(method) = member
638-
&& let MethodBody::Concrete(block) = &method.body
639-
{
640-
let body_start = block.left_brace.start.offset;
641-
let body_end = block.right_brace.end.offset;
642-
if offset >= body_start && offset <= body_end {
643-
return collect_function_scope(
644-
&method.parameter_list,
645-
block.statements.as_slice(),
646-
body_start,
647-
body_end,
648-
);
649-
}
650-
}
651-
}
652-
}
653-
}
654-
}
655-
}
656-
657-
// Fallback: top-level scope
658-
let body_end = content.len() as u32;
659-
collect_scope(program.statements.as_slice(), 0, body_end)
586+
with_parsed_program(content, "extract_variable", |program, content| {
587+
crate::scope_collector::build_scope_map_for_offset(
588+
program.statements.as_slice(),
589+
offset,
590+
content.len() as u32,
591+
)
660592
})
661593
}
662594

src/code_actions/inline_variable.rs

Lines changed: 7 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use tower_lsp::lsp_types::*;
2727
use crate::Backend;
2828
use crate::code_actions::{CodeActionData, make_code_action_data};
2929
use crate::parser::with_parsed_program;
30-
use crate::scope_collector::{AccessKind, ScopeMap, collect_function_scope, collect_scope};
30+
use crate::scope_collector::{AccessKind, ScopeMap};
3131
use crate::util::{offset_to_position, position_to_byte_offset};
3232

3333
// ─── AST helpers ────────────────────────────────────────────────────────────
@@ -487,104 +487,15 @@ fn expression_has_side_effects(expr: &Expression<'_>) -> bool {
487487
/// Build a `ScopeMap` for the file by walking the AST, identical to the
488488
/// approach used in extract_variable.
489489
fn build_scope_map(content: &str, offset: u32) -> ScopeMap {
490-
with_parsed_program(content, "inline_variable", |program, _content| {
491-
// Try to find the enclosing function or method.
492-
for stmt in program.statements.iter() {
493-
if let Some(map) = try_build_scope_from_statement(stmt, offset) {
494-
return map;
495-
}
496-
}
497-
498-
// Fallback: top-level scope.
499-
let body_end = content.len() as u32;
500-
collect_scope(program.statements.as_slice(), 0, body_end)
490+
with_parsed_program(content, "inline_variable", |program, content| {
491+
crate::scope_collector::build_scope_map_for_offset(
492+
program.statements.as_slice(),
493+
offset,
494+
content.len() as u32,
495+
)
501496
})
502497
}
503498

504-
fn try_build_scope_from_statement(stmt: &Statement<'_>, offset: u32) -> Option<ScopeMap> {
505-
match stmt {
506-
Statement::Function(func) => {
507-
let body_start = func.body.left_brace.start.offset;
508-
let body_end = func.body.right_brace.end.offset;
509-
if offset >= body_start && offset <= body_end {
510-
return Some(collect_function_scope(
511-
&func.parameter_list,
512-
func.body.statements.as_slice(),
513-
body_start,
514-
body_end,
515-
));
516-
}
517-
None
518-
}
519-
Statement::Class(class) => {
520-
for member in class.members.iter() {
521-
if let ClassLikeMember::Method(method) = member
522-
&& let MethodBody::Concrete(block) = &method.body
523-
{
524-
let body_start = block.left_brace.start.offset;
525-
let body_end = block.right_brace.end.offset;
526-
if offset >= body_start && offset <= body_end {
527-
return Some(collect_function_scope(
528-
&method.parameter_list,
529-
block.statements.as_slice(),
530-
body_start,
531-
body_end,
532-
));
533-
}
534-
}
535-
}
536-
None
537-
}
538-
Statement::Trait(tr) => {
539-
for member in tr.members.iter() {
540-
if let ClassLikeMember::Method(method) = member
541-
&& let MethodBody::Concrete(block) = &method.body
542-
{
543-
let body_start = block.left_brace.start.offset;
544-
let body_end = block.right_brace.end.offset;
545-
if offset >= body_start && offset <= body_end {
546-
return Some(collect_function_scope(
547-
&method.parameter_list,
548-
block.statements.as_slice(),
549-
body_start,
550-
body_end,
551-
));
552-
}
553-
}
554-
}
555-
None
556-
}
557-
Statement::Enum(en) => {
558-
for member in en.members.iter() {
559-
if let ClassLikeMember::Method(method) = member
560-
&& let MethodBody::Concrete(block) = &method.body
561-
{
562-
let body_start = block.left_brace.start.offset;
563-
let body_end = block.right_brace.end.offset;
564-
if offset >= body_start && offset <= body_end {
565-
return Some(collect_function_scope(
566-
&method.parameter_list,
567-
block.statements.as_slice(),
568-
body_start,
569-
body_end,
570-
));
571-
}
572-
}
573-
}
574-
None
575-
}
576-
Statement::Namespace(ns) => {
577-
for inner_stmt in ns.statements().iter() {
578-
if let Some(map) = try_build_scope_from_statement(inner_stmt, offset) {
579-
return Some(map);
580-
}
581-
}
582-
None
583-
}
584-
_ => None,
585-
}
586-
}
587-
588499
// ─── Line deletion helpers ──────────────────────────────────────────────────
589500

590501
/// Compute the byte range for deleting an entire statement line.

0 commit comments

Comments
 (0)