php_lsp/actions/
extract_interface_action.rs1use std::collections::HashMap;
8
9use php_ast::{ClassMemberKind, NamespaceBody, Stmt, StmtKind, Visibility};
10use tower_lsp_server::ls_types::{
11 CodeAction, CodeActionKind, CodeActionOrCommand, Range, TextEdit, Uri, WorkspaceEdit,
12};
13
14use crate::document::ast::{ParsedDoc, SourceView, format_type_hint};
15use crate::hover::format_params_str;
16use crate::text::fqn_short_name;
17
18struct MethodSig {
19 name: String,
20 is_static: bool,
21 by_ref: bool,
22 params: String,
23 return_type: Option<String>,
24}
25
26pub fn extract_interface_actions(
27 source: &str,
28 doc: &ParsedDoc,
29 range: Range,
30 uri: &Uri,
31) -> Vec<CodeActionOrCommand> {
32 let sv = doc.view();
33 let mut out = Vec::new();
34 let root = &doc.program().stmts;
35 collect(root, root, source, sv, range, uri, &mut out);
36 out
37}
38
39fn collect<'a>(
40 stmts: &[Stmt<'a, 'a>],
41 root: &[Stmt<'a, 'a>],
42 source: &str,
43 sv: SourceView<'_>,
44 range: Range,
45 uri: &Uri,
46 out: &mut Vec<CodeActionOrCommand>,
47) {
48 for stmt in stmts {
49 match &stmt.kind {
50 StmtKind::Class(c) => {
51 let class_start_line = sv.position_of(stmt.span.start).line;
52 let brace_line = sv.position_of(c.body.span.start).line;
53 if range.start.line < class_start_line || range.start.line > brace_line {
54 continue;
55 }
56
57 let Some(class_name) = &c.name else { continue };
58 let class_name_str = class_name.to_string();
59 let interface_name = format!("{class_name_str}Interface");
60
61 if c.implements.iter().any(|iface| {
63 let repr = iface.to_string_repr();
64 repr == interface_name || repr == format!("\\{interface_name}")
65 }) {
66 continue;
67 }
68
69 let mut methods = public_method_sigs(c.body.members.iter());
70 let mut seen: std::collections::HashSet<String> =
71 methods.iter().map(|m| m.name.clone()).collect();
72
73 for member in c.body.members.iter() {
78 if let ClassMemberKind::TraitUse(tu) = &member.kind {
79 for trait_name in tu.traits.iter() {
80 let short = fqn_short_name(&trait_name.to_string_repr()).to_string();
81 for m in find_trait_methods(root, &short) {
82 if seen.insert(m.name.clone()) {
83 methods.push(m);
84 }
85 }
86 }
87 }
88 }
89
90 if methods.is_empty() {
91 continue;
92 }
93
94 let insert_pos = sv.position_of(stmt.span.start);
96 let interface_text = generate_interface_text(&interface_name, &methods);
97 let interface_edit = TextEdit {
98 range: Range {
99 start: insert_pos,
100 end: insert_pos,
101 },
102 new_text: format!("{interface_text}\n\n"),
103 };
104
105 let brace_byte = c.body.span.start as usize;
111 let trimmed_len = source[..brace_byte]
112 .trim_end_matches(|ch: char| ch.is_ascii_whitespace())
113 .len();
114 let insert_implements_pos = sv.position_of(trimmed_len as u32);
115 let implements_text = if c.implements.is_empty() {
116 format!(" implements {interface_name}")
117 } else {
118 format!(", {interface_name}")
119 };
120 let implements_edit = TextEdit {
121 range: Range {
122 start: insert_implements_pos,
123 end: insert_implements_pos,
124 },
125 new_text: implements_text,
126 };
127
128 let mut changes = HashMap::new();
129 changes.insert(uri.clone(), vec![interface_edit, implements_edit]);
130
131 out.push(CodeActionOrCommand::CodeAction(CodeAction {
132 title: format!("Extract interface '{interface_name}'"),
133 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
134 edit: Some(WorkspaceEdit {
135 changes: Some(changes),
136 ..Default::default()
137 }),
138 ..Default::default()
139 }));
140 }
141 StmtKind::Namespace(ns) => {
142 if let NamespaceBody::Braced(inner) = &ns.body {
143 collect(&inner.stmts, root, source, sv, range, uri, out);
144 }
145 }
146 _ => {}
147 }
148 }
149}
150
151fn public_method_sigs<'a>(
154 members: impl Iterator<Item = &'a php_ast::ClassMember<'a, 'a>>,
155) -> Vec<MethodSig> {
156 members
157 .filter_map(|member| {
158 let ClassMemberKind::Method(m) = &member.kind else {
159 return None;
160 };
161 if !matches!(m.visibility, Some(Visibility::Public) | None) {
163 return None;
164 }
165 let name = m.name.to_string();
166 if name == "__construct" || name == "__destruct" {
167 return None;
168 }
169 Some(MethodSig {
170 name,
171 is_static: m.is_static,
172 by_ref: m.by_ref,
173 params: format_params_str(&m.params),
174 return_type: m.return_type.as_ref().map(format_type_hint),
175 })
176 })
177 .collect()
178}
179
180fn find_trait_methods<'a>(stmts: &[Stmt<'a, 'a>], name: &str) -> Vec<MethodSig> {
184 for stmt in stmts {
185 match &stmt.kind {
186 StmtKind::Trait(t) if t.name.as_str() == Some(name) => {
187 return public_method_sigs(t.body.members.iter());
188 }
189 StmtKind::Namespace(ns) => {
190 if let NamespaceBody::Braced(inner) = &ns.body {
191 let found = find_trait_methods(&inner.stmts, name);
192 if !found.is_empty() {
193 return found;
194 }
195 }
196 }
197 _ => {}
198 }
199 }
200 Vec::new()
201}
202
203fn generate_interface_text(name: &str, methods: &[MethodSig]) -> String {
204 let mut text = format!("interface {name}\n{{\n");
205 for m in methods {
206 let static_kw = if m.is_static { "static " } else { "" };
207 let by_ref = if m.by_ref { "&" } else { "" };
208 let ret = match &m.return_type {
209 Some(t) => format!(": {t}"),
210 None => String::new(),
211 };
212 text.push_str(&format!(
213 " public {}function {}{}({}){ret};\n",
214 static_kw, by_ref, m.name, m.params,
215 ));
216 }
217 text.push('}');
218 text
219}