Skip to content

Commit cc20ae2

Browse files
committed
Clean up code actions
1 parent de64839 commit cc20ae2

41 files changed

Lines changed: 487 additions & 940 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/todo/refactor.md

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -350,54 +350,4 @@ constructor.
350350

351351
---
352352

353-
## Code actions: shared edit-building helpers
354-
355-
**What to do.** Complementary to (and independent of) A34 in the
356-
backlog:
357-
358-
1. **`single_file_edit` helper.** `WorkspaceEdit` for one file is
359-
open-coded in ~35 files (~50 `document_changes: None` literals,
360-
8-12 lines each). Add
361-
`single_file_edit(uri, edits) -> WorkspaceEdit` /
362-
`single_edit(uri, range, text)` to `code_actions/mod.rs` and adopt.
363-
2. **Adopt `util::byte_range_to_lsp_range`.** The helper exists but is
364-
used in code actions only by `phpstan/add_throws.rs`; ~29 other
365-
files hand-build `Range { start: offset_to_position(..), end: .. }`
366-
(183 `offset_to_position` calls).
367-
3. **Consolidate indent helpers.** Nine near-identical
368-
line-indent-at-offset extractors and two copies of
369-
`detect_indent_unit` across `extract_function.rs`,
370-
`generate_property_hooks.rs`, `update_docblock.rs`,
371-
`convert_switch_to_match.rs`, `extract_constant.rs`,
372-
`phpstan/new_static.rs`, `implement_methods.rs`. Provide
373-
`indent_of_line_at` / `indent_unit` next to
374-
`detect_indent_from_members`.
375-
4. **Unify the name-deduplication helpers.** `deduplicate_name` in
376-
`extract_variable.rs` (checks a `&[String]` of in-scope names,
377-
numeric suffix), `deduplicate_constant_name` in `extract_constant.rs`
378-
(same shape, `_N` suffix), and `deduplicate_name` in
379-
`extract_function.rs` (scans sibling method names or `function <name>`
380-
patterns in the file) all solve "make this name unique." Back them
381-
with one helper in `code_actions/naming.rs` that takes an
382-
`exists: impl Fn(&str) -> bool` predicate and a suffix separator. (The
383-
pure casing transforms — `to_camel_case`, `snake_to_camel`,
384-
`to_pascal_case`, `string_to_screaming_snake`, `capitalise` — already
385-
live in `code_actions/naming.rs`.)
386-
5. **`find_docblock_above_line` helper.** At least three independent
387-
copies (`phpstan/remove_throws.rs`, `phpstan/add_throws.rs`,
388-
`phpstan/add_iterable_type.rs`) locate the `/** */` block above a
389-
line; `update_docblock.rs` additionally owns a private docblock
390-
line-model (`parse_docblock_lines`/`rebuild_docblock`) that the
391-
other handlers re-approximate. Extract a shared
392-
`code_actions/docblock_edit.rs`.
393-
6. **Relocate PHPStan-specific logic out of `code_actions/mod.rs`**
394-
(`expand_sibling_checked_exception_diags`,
395-
`clear_phpstan_diagnostics_after_resolve`) into `phpstan/mod.rs`.
396-
397-
**Why it matters.** Several new code actions are scheduled (A40, A41,
398-
H4-H24, FX rules); each currently starts by copying this plumbing from
399-
a neighbour, adding another divergent copy per action.
400-
401-
---
402-
403353

src/code_actions/change_visibility.rs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
//! This is a single-file edit — it does not update call sites or
2525
//! subclass overrides in other files.
2626
27-
use std::collections::HashMap;
2827
use std::sync::Arc;
2928

3029
#[cfg(test)]
@@ -227,23 +226,14 @@ impl Backend {
227226
let end_pos = offset_to_position(content, vis_end);
228227

229228
let doc_uri: Url = uri.parse().ok()?;
230-
let mut changes = HashMap::new();
231-
changes.insert(
229+
Some(crate::code_actions::single_edit(
232230
doc_uri,
233-
vec![TextEdit {
234-
range: Range {
235-
start: start_pos,
236-
end: end_pos,
237-
},
238-
new_text: target_vis.to_string(),
239-
}],
240-
);
241-
242-
Some(WorkspaceEdit {
243-
changes: Some(changes),
244-
document_changes: None,
245-
change_annotations: None,
246-
})
231+
Range {
232+
start: start_pos,
233+
end: end_pos,
234+
},
235+
target_vis.to_string(),
236+
))
247237
}
248238

249239
// ── Private helpers ─────────────────────────────────────────────────

src/code_actions/convert_switch_to_match.rs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
//! - No case body falls through to the next without `break`/`return`/`throw`.
1010
//! - The switch subject is a simple expression.
1111
12-
use std::collections::HashMap;
13-
1412
use mago_span::HasSpan;
1513
use mago_syntax::cst::*;
1614
use tower_lsp::lsp_types::*;
@@ -60,27 +58,18 @@ impl Backend {
6058
let start_pos = offset_to_position(content, switch_start as usize);
6159
let end_pos = offset_to_position(content, switch_end as usize);
6260

63-
let mut changes = HashMap::new();
64-
changes.insert(
65-
doc_uri,
66-
vec![TextEdit {
67-
range: Range {
68-
start: start_pos,
69-
end: end_pos,
70-
},
71-
new_text: replacement,
72-
}],
73-
);
74-
7561
out.push(CodeActionOrCommand::CodeAction(CodeAction {
7662
title: "Convert to match expression".to_string(),
7763
kind: Some(CodeActionKind::new("refactor.rewrite")),
7864
diagnostics: None,
79-
edit: Some(WorkspaceEdit {
80-
changes: Some(changes),
81-
document_changes: None,
82-
change_annotations: None,
83-
}),
65+
edit: Some(crate::code_actions::single_edit(
66+
doc_uri,
67+
Range {
68+
start: start_pos,
69+
end: end_pos,
70+
},
71+
replacement,
72+
)),
8473
command: None,
8574
is_preferred: Some(false),
8675
disabled: None,

src/code_actions/convert_to_arrow_function.rs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
//! - The closure does not have a `void` or `never` return type hint.
1010
//! - PHP version is >= 7.4.
1111
12-
use std::collections::HashMap;
13-
1412
use mago_span::HasSpan;
1513
use mago_syntax::cst::access::Access;
1614
use mago_syntax::cst::call::Call;
@@ -62,27 +60,18 @@ impl Backend {
6260
let start_pos = offset_to_position(content, closure_start as usize);
6361
let end_pos = offset_to_position(content, closure_end as usize);
6462

65-
let mut changes = HashMap::new();
66-
changes.insert(
67-
doc_uri,
68-
vec![TextEdit {
69-
range: Range {
70-
start: start_pos,
71-
end: end_pos,
72-
},
73-
new_text: replacement,
74-
}],
75-
);
76-
7763
out.push(CodeActionOrCommand::CodeAction(CodeAction {
7864
title: "Convert to arrow function".to_string(),
7965
kind: Some(CodeActionKind::new("refactor.rewrite")),
8066
diagnostics: None,
81-
edit: Some(WorkspaceEdit {
82-
changes: Some(changes),
83-
document_changes: None,
84-
change_annotations: None,
85-
}),
67+
edit: Some(crate::code_actions::single_edit(
68+
doc_uri,
69+
Range {
70+
start: start_pos,
71+
end: end_pos,
72+
},
73+
replacement,
74+
)),
8675
command: None,
8776
is_preferred: Some(false),
8877
disabled: None,

src/code_actions/convert_to_closure.rs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//!
1010
//! The action is always offered when the cursor is on an arrow function.
1111
12-
use std::collections::{HashMap, HashSet};
12+
use std::collections::HashSet;
1313

1414
use mago_span::HasSpan;
1515
use mago_syntax::cst::access::Access;
@@ -57,27 +57,18 @@ impl Backend {
5757
let start_pos = offset_to_position(content, arrow_start as usize);
5858
let end_pos = offset_to_position(content, arrow_end as usize);
5959

60-
let mut changes = HashMap::new();
61-
changes.insert(
62-
doc_uri,
63-
vec![TextEdit {
64-
range: Range {
65-
start: start_pos,
66-
end: end_pos,
67-
},
68-
new_text: replacement,
69-
}],
70-
);
71-
7260
out.push(CodeActionOrCommand::CodeAction(CodeAction {
7361
title: "Convert to closure".to_string(),
7462
kind: Some(CodeActionKind::new("refactor.rewrite")),
7563
diagnostics: None,
76-
edit: Some(WorkspaceEdit {
77-
changes: Some(changes),
78-
document_changes: None,
79-
change_annotations: None,
80-
}),
64+
edit: Some(crate::code_actions::single_edit(
65+
doc_uri,
66+
Range {
67+
start: start_pos,
68+
end: end_pos,
69+
},
70+
replacement,
71+
)),
8172
command: None,
8273
is_preferred: Some(false),
8374
disabled: None,

src/code_actions/convert_to_instance_variable.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
//! - The `$this` variable is never offered for conversion.
1515
//! - Only works inside a method body of a class-like declaration.
1616
17-
use std::collections::HashMap;
18-
1917
use mago_span::HasSpan;
2018
use mago_syntax::cst::class_like::member::ClassLikeMember;
2119
use mago_syntax::cst::class_like::method::MethodBody;
@@ -511,14 +509,7 @@ impl Backend {
511509
.then(a.range.start.character.cmp(&b.range.start.character))
512510
});
513511

514-
let mut changes = HashMap::new();
515-
changes.insert(doc_uri, edits);
516-
517-
Some(WorkspaceEdit {
518-
changes: Some(changes),
519-
document_changes: None,
520-
change_annotations: None,
521-
})
512+
Some(crate::code_actions::single_file_edit(doc_uri, edits))
522513
}
523514
}
524515

src/code_actions/docblock_edit.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//! Shared docblock-location helpers for code actions.
2+
//!
3+
//! Several PHPStan quickfixes need to locate the `/** … */` block that sits
4+
//! directly above a function, method, or property signature so they can
5+
//! rewrite or remove tags inside it. This module owns the single
6+
//! implementation they share.
7+
8+
/// Information about a docblock found above a given line.
9+
pub(crate) struct DocblockAbove {
10+
/// Byte offset of the start of the docblock (first char of the `/**`
11+
/// line, including leading whitespace).
12+
pub(crate) start: usize,
13+
/// Byte offset just past the end of the docblock (past the `*/` line,
14+
/// including its trailing newline).
15+
pub(crate) end: usize,
16+
/// The raw docblock text including indentation.
17+
pub(crate) text: String,
18+
}
19+
20+
/// Find the docblock immediately above the given line.
21+
///
22+
/// The diagnostic line is the function/method/property signature. The
23+
/// docblock (if any) sits directly above it, possibly separated by blank
24+
/// lines or attribute (`#[…]`) lines.
25+
pub(crate) fn find_docblock_above_line(content: &str, line: usize) -> Option<DocblockAbove> {
26+
let lines: Vec<&str> = content.lines().collect();
27+
if line == 0 || line > lines.len() {
28+
return None;
29+
}
30+
31+
// Walk backward from the line before the diagnostic to find `*/`.
32+
let mut doc_end_line = None;
33+
for i in (0..line).rev() {
34+
let trimmed = lines[i].trim();
35+
if trimmed.is_empty() {
36+
continue;
37+
}
38+
if trimmed.ends_with("*/") {
39+
doc_end_line = Some(i);
40+
break;
41+
}
42+
// Attributes (#[...]) can appear between docblock and declaration.
43+
if trimmed.starts_with("#[") {
44+
continue;
45+
}
46+
// Anything else means no docblock.
47+
break;
48+
}
49+
50+
let end_line = doc_end_line?;
51+
52+
// Walk backward from end_line to find `/**`.
53+
let mut doc_start_line = None;
54+
for i in (0..=end_line).rev() {
55+
let trimmed = lines[i].trim();
56+
if trimmed.contains("/**") {
57+
doc_start_line = Some(i);
58+
break;
59+
}
60+
// Should be a `*`-prefixed line or end-of-docblock.
61+
if !trimmed.starts_with('*') && !trimmed.ends_with("*/") {
62+
break;
63+
}
64+
}
65+
66+
let start_line = doc_start_line?;
67+
68+
// Convert line numbers to byte offsets.
69+
let mut byte_offset = 0;
70+
let mut start_byte = 0;
71+
let mut end_byte = 0;
72+
for (i, line_text) in lines.iter().enumerate() {
73+
if i == start_line {
74+
start_byte = byte_offset;
75+
}
76+
byte_offset += line_text.len() + 1; // +1 for newline
77+
if i == end_line {
78+
end_byte = byte_offset; // include trailing newline
79+
}
80+
}
81+
82+
let text = content
83+
.get(start_byte..end_byte.min(content.len()))
84+
.unwrap_or("")
85+
.to_string();
86+
87+
Some(DocblockAbove {
88+
start: start_byte,
89+
end: end_byte.min(content.len()),
90+
text,
91+
})
92+
}

0 commit comments

Comments
 (0)