1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use php_ast::{ClassMemberKind, NamespaceBody, Stmt, StmtKind, Visibility};
10use tower_lsp::lsp_types::{
11 CodeAction, CodeActionKind, CodeActionOrCommand, Range, TextEdit, Url, 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 MethodStub {
19 name: String,
20 visibility: &'static str,
21 is_static: bool,
22 params: String,
23 return_type: Option<String>,
24}
25
26pub fn implement_missing_actions(
27 _source: &str,
28 doc: &ParsedDoc,
29 all_docs: &[(Url, Arc<ParsedDoc>)],
30 range: Range,
31 uri: &Url,
32 file_imports: &HashMap<String, String>,
33) -> Vec<CodeActionOrCommand> {
34 let sv = doc.view();
35 let mut actions = Vec::new();
36 collect_actions(
37 &doc.program().stmts,
38 sv,
39 all_docs,
40 file_imports,
41 range,
42 uri,
43 &mut actions,
44 );
45 actions
46}
47
48fn collect_actions(
49 stmts: &[Stmt<'_, '_>],
50 sv: SourceView<'_>,
51 all_docs: &[(Url, Arc<ParsedDoc>)],
52 file_imports: &HashMap<String, String>,
53 range: Range,
54 uri: &Url,
55 out: &mut Vec<CodeActionOrCommand>,
56) {
57 for stmt in stmts {
58 match &stmt.kind {
59 StmtKind::Class(c) => {
60 let class_start = sv.position_of(stmt.span.start).line;
61 let class_end = sv.position_of(stmt.span.end).line;
62 if class_start > range.end.line || class_end < range.start.line {
63 continue;
64 }
65
66 let existing: HashSet<String> = c
67 .body
68 .members
69 .iter()
70 .filter_map(|m| {
71 if let ClassMemberKind::Method(method) = &m.kind {
72 Some(method.name.to_string())
73 } else {
74 None
75 }
76 })
77 .collect();
78
79 let mut missing: Vec<MethodStub> = Vec::new();
80
81 for iface in c.implements.iter() {
82 let iface_name = iface.to_string_repr().into_owned();
83 let short = fqn_short_name(&iface_name).to_string();
84 let fqn = file_imports.get(&short).cloned();
86 for stub in abstract_methods_of(&short, fqn.as_deref(), all_docs) {
87 if !existing.contains(&stub.name) {
88 missing.push(stub);
89 }
90 }
91 }
92
93 if let Some(parent) = &c.extends {
94 let parent_name = parent.to_string_repr().into_owned();
95 let short = fqn_short_name(&parent_name).to_string();
96 let fqn = file_imports.get(&short).cloned();
97 for stub in abstract_methods_of(&short, fqn.as_deref(), all_docs) {
98 if !existing.contains(&stub.name) {
99 missing.push(stub);
100 }
101 }
102 }
103
104 {
106 let mut seen = HashSet::new();
107 missing.retain(|s| seen.insert(s.name.clone()));
108 }
109
110 if missing.is_empty() {
111 continue;
112 }
113
114 let mut stub_text = generate_stub_text(&missing);
115 let closing_pos = sv.position_of(stmt.span.end.saturating_sub(1));
116 let insert_pos = closing_pos;
117 if closing_pos.character > 0 {
121 stub_text = format!("\n{stub_text}");
122 }
123 let edit = TextEdit {
124 range: Range {
125 start: insert_pos,
126 end: insert_pos,
127 },
128 new_text: stub_text,
129 };
130 let mut changes = HashMap::new();
131 changes.insert(uri.clone(), vec![edit]);
132
133 let n = missing.len();
134 let title = if n == 1 {
135 "Implement missing method".to_string()
136 } else {
137 format!("Implement {n} missing methods")
138 };
139 out.push(CodeActionOrCommand::CodeAction(CodeAction {
140 title,
141 kind: Some(CodeActionKind::QUICKFIX),
142 is_preferred: Some(true),
147 edit: Some(WorkspaceEdit {
148 changes: Some(changes),
149 ..Default::default()
150 }),
151 ..Default::default()
152 }));
153 }
154 StmtKind::Namespace(ns) => {
155 if let NamespaceBody::Braced(inner) = &ns.body {
156 collect_actions(&inner.stmts, sv, all_docs, file_imports, range, uri, out);
157 }
158 }
159 _ => {}
160 }
161 }
162}
163
164fn abstract_methods_of(
174 name: &str,
175 fqn: Option<&str>,
176 all_docs: &[(Url, Arc<ParsedDoc>)],
177) -> Vec<MethodStub> {
178 if let Some(fqn) = fqn {
179 for (_, doc) in all_docs {
182 if let Some(stubs) = collect_abstract_methods_fqn(&doc.program().stmts, fqn, "") {
183 return stubs;
184 }
185 }
186 return vec![];
187 }
188
189 for (_, doc) in all_docs {
191 if let Some(stubs) = collect_abstract_methods(&doc.program().stmts, name) {
192 return stubs;
193 }
194 }
195 vec![]
196}
197
198fn collect_abstract_methods_fqn(
202 stmts: &[Stmt<'_, '_>],
203 fqn: &str,
204 current_ns: &str,
205) -> Option<Vec<MethodStub>> {
206 let short = fqn_short_name(fqn);
208 let mut active_ns = current_ns.to_string();
211
212 for stmt in stmts {
213 match &stmt.kind {
214 StmtKind::Interface(i) if i.name == short => {
215 let declared_fqn = if active_ns.is_empty() {
217 i.name.to_string()
218 } else {
219 format!("{}\\{}", active_ns, &i.name.to_string())
220 };
221 if fqn_eq(fqn, &declared_fqn) {
222 let stubs = i
223 .body
224 .members
225 .iter()
226 .filter_map(|m| {
227 if let ClassMemberKind::Method(method) = &m.kind {
228 Some(MethodStub {
229 name: method.name.to_string(),
230 visibility: "public",
231 is_static: method.is_static,
232 params: format_params_str(&method.params),
233 return_type: method
234 .return_type
235 .as_ref()
236 .map(|t| format_type_hint(t)),
237 })
238 } else {
239 None
240 }
241 })
242 .collect();
243 return Some(stubs);
244 }
245 }
246 StmtKind::Class(c)
247 if c.name.as_ref().map(|n| n.to_string()) == Some(short.to_string())
248 && c.modifiers.is_abstract =>
249 {
250 let declared_fqn = if active_ns.is_empty() {
251 short.to_string()
252 } else {
253 format!("{}\\{}", active_ns, short)
254 };
255 if fqn_eq(fqn, &declared_fqn) {
256 let stubs = c
257 .body
258 .members
259 .iter()
260 .filter_map(|m| {
261 if let ClassMemberKind::Method(method) = &m.kind {
262 if method.is_abstract {
263 Some(MethodStub {
264 name: method.name.to_string(),
265 visibility: visibility_str(method.visibility.as_ref()),
266 is_static: method.is_static,
267 params: format_params_str(&method.params),
268 return_type: method
269 .return_type
270 .as_ref()
271 .map(|t| format_type_hint(t)),
272 })
273 } else {
274 None
275 }
276 } else {
277 None
278 }
279 })
280 .collect();
281 return Some(stubs);
282 }
283 }
284 StmtKind::Namespace(ns) => {
285 let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().into_owned());
286 match &ns.body {
287 NamespaceBody::Braced(inner) => {
288 let child_ns = match &ns_name {
289 Some(n) if !active_ns.is_empty() => {
290 format!("{}\\{}", active_ns, n)
291 }
292 Some(n) => n.clone(),
293 None => active_ns.clone(),
294 };
295 if let Some(stubs) =
296 collect_abstract_methods_fqn(&inner.stmts, fqn, &child_ns)
297 {
298 return Some(stubs);
299 }
300 }
301 NamespaceBody::Simple => {
302 active_ns = ns_name.unwrap_or_default();
304 }
305 }
306 }
307 _ => {}
308 }
309 }
310 None
311}
312
313fn fqn_eq(a: &str, b: &str) -> bool {
315 a.trim_start_matches('\\') == b.trim_start_matches('\\')
316}
317
318fn collect_abstract_methods(stmts: &[Stmt<'_, '_>], name: &str) -> Option<Vec<MethodStub>> {
319 for stmt in stmts {
320 match &stmt.kind {
321 StmtKind::Interface(i) if i.name == name => {
322 let stubs = i
323 .body
324 .members
325 .iter()
326 .filter_map(|m| {
327 if let ClassMemberKind::Method(method) = &m.kind {
328 Some(MethodStub {
329 name: method.name.to_string(),
330 visibility: "public",
331 is_static: method.is_static,
332 params: format_params_str(&method.params),
333 return_type: method
334 .return_type
335 .as_ref()
336 .map(|t| format_type_hint(t)),
337 })
338 } else {
339 None
340 }
341 })
342 .collect();
343 return Some(stubs);
344 }
345 StmtKind::Class(c)
346 if c.name.as_ref().map(|n| n.to_string()) == Some(name.to_string())
347 && c.modifiers.is_abstract =>
348 {
349 let stubs = c
350 .body
351 .members
352 .iter()
353 .filter_map(|m| {
354 if let ClassMemberKind::Method(method) = &m.kind {
355 if method.is_abstract {
356 Some(MethodStub {
357 name: method.name.to_string(),
358 visibility: visibility_str(method.visibility.as_ref()),
359 is_static: method.is_static,
360 params: format_params_str(&method.params),
361 return_type: method
362 .return_type
363 .as_ref()
364 .map(|t| format_type_hint(t)),
365 })
366 } else {
367 None
368 }
369 } else {
370 None
371 }
372 })
373 .collect();
374 return Some(stubs);
375 }
376 StmtKind::Namespace(ns) => {
377 if let NamespaceBody::Braced(inner) = &ns.body
378 && let Some(stubs) = collect_abstract_methods(&inner.stmts, name)
379 {
380 return Some(stubs);
381 }
382 }
383 _ => {}
384 }
385 }
386 None
387}
388
389fn visibility_str(v: Option<&Visibility>) -> &'static str {
390 match v {
391 Some(Visibility::Protected) => "protected",
392 Some(Visibility::Private) => "private",
393 _ => "public",
394 }
395}
396
397fn generate_stub_text(stubs: &[MethodStub]) -> String {
398 let mut text = String::new();
399 for stub in stubs {
400 let static_kw = if stub.is_static { "static " } else { "" };
401 let ret = match &stub.return_type {
402 Some(t) => format!(": {t}"),
403 None => String::new(),
404 };
405 text.push_str(&format!(
406 " {} {}function {}({}){ret}\n {{\n throw new \\RuntimeException('Not implemented');\n }}\n\n",
407 stub.visibility, static_kw, stub.name, stub.params
408 ));
409 }
410 text
411}