1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
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 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: &[(Uri, Arc<ParsedDoc>)],
30 range: Range,
31 uri: &Uri,
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
48#[cfg_attr(target_family = "wasm", allow(dead_code))]
53pub(crate) fn target_type_names(
54 stmts: &[Stmt<'_, '_>],
55 sv: SourceView<'_>,
56 range: Range,
57) -> Vec<String> {
58 let mut names = Vec::new();
59 collect_target_type_names(stmts, sv, range, &mut names);
60 names
61}
62
63#[cfg_attr(target_family = "wasm", allow(dead_code))]
64fn collect_target_type_names(
65 stmts: &[Stmt<'_, '_>],
66 sv: SourceView<'_>,
67 range: Range,
68 out: &mut Vec<String>,
69) {
70 for stmt in stmts {
71 match &stmt.kind {
72 StmtKind::Class(c) => {
73 let class_start = sv.position_of(stmt.span.start).line;
74 let class_end = sv.position_of(stmt.span.end).line;
75 if class_start > range.end.line || class_end < range.start.line {
76 continue;
77 }
78 for iface in c.implements.iter() {
79 out.push(fqn_short_name(&iface.to_string_repr()).to_string());
80 }
81 if let Some(parent) = &c.extends {
82 out.push(fqn_short_name(&parent.to_string_repr()).to_string());
83 }
84 }
85 StmtKind::Namespace(ns) => {
86 if let NamespaceBody::Braced(inner) = &ns.body {
87 collect_target_type_names(&inner.stmts, sv, range, out);
88 }
89 }
90 _ => {}
91 }
92 }
93}
94
95fn collect_actions(
96 stmts: &[Stmt<'_, '_>],
97 sv: SourceView<'_>,
98 all_docs: &[(Uri, Arc<ParsedDoc>)],
99 file_imports: &HashMap<String, String>,
100 range: Range,
101 uri: &Uri,
102 out: &mut Vec<CodeActionOrCommand>,
103) {
104 for stmt in stmts {
105 match &stmt.kind {
106 StmtKind::Class(c) => {
107 let class_start = sv.position_of(stmt.span.start).line;
108 let class_end = sv.position_of(stmt.span.end).line;
109 if class_start > range.end.line || class_end < range.start.line {
110 continue;
111 }
112
113 let existing: HashSet<String> = c
114 .body
115 .members
116 .iter()
117 .filter_map(|m| {
118 if let ClassMemberKind::Method(method) = &m.kind {
119 Some(method.name.to_string())
120 } else {
121 None
122 }
123 })
124 .collect();
125
126 let mut missing: Vec<MethodStub> = Vec::new();
127
128 for iface in c.implements.iter() {
129 let iface_name = iface.to_string_repr().into_owned();
130 let short = fqn_short_name(&iface_name).to_string();
131 let fqn = file_imports.get(&short).cloned();
133 for stub in abstract_methods_of(&short, fqn.as_deref(), all_docs) {
134 if !existing.contains(&stub.name) {
135 missing.push(stub);
136 }
137 }
138 }
139
140 if let Some(parent) = &c.extends {
141 let parent_name = parent.to_string_repr().into_owned();
142 let short = fqn_short_name(&parent_name).to_string();
143 let fqn = file_imports.get(&short).cloned();
144 for stub in abstract_methods_of(&short, fqn.as_deref(), all_docs) {
145 if !existing.contains(&stub.name) {
146 missing.push(stub);
147 }
148 }
149 }
150
151 {
153 let mut seen = HashSet::new();
154 missing.retain(|s| seen.insert(s.name.clone()));
155 }
156
157 if missing.is_empty() {
158 continue;
159 }
160
161 let mut stub_text = generate_stub_text(&missing);
162 let closing_pos = sv.position_of(stmt.span.end.saturating_sub(1));
163 let insert_pos = closing_pos;
164 if closing_pos.character > 0 {
168 stub_text = format!("\n{stub_text}");
169 }
170 let edit = TextEdit {
171 range: Range {
172 start: insert_pos,
173 end: insert_pos,
174 },
175 new_text: stub_text,
176 };
177 let mut changes = HashMap::new();
178 changes.insert(uri.clone(), vec![edit]);
179
180 let n = missing.len();
181 let title = if n == 1 {
182 "Implement missing method".to_string()
183 } else {
184 format!("Implement {n} missing methods")
185 };
186 out.push(CodeActionOrCommand::CodeAction(CodeAction {
187 title,
188 kind: Some(CodeActionKind::QUICKFIX),
189 is_preferred: Some(true),
194 edit: Some(WorkspaceEdit {
195 changes: Some(changes),
196 ..Default::default()
197 }),
198 ..Default::default()
199 }));
200 }
201 StmtKind::Namespace(ns) => {
202 if let NamespaceBody::Braced(inner) = &ns.body {
203 collect_actions(&inner.stmts, sv, all_docs, file_imports, range, uri, out);
204 }
205 }
206 _ => {}
207 }
208 }
209}
210
211fn abstract_methods_of(
221 name: &str,
222 fqn: Option<&str>,
223 all_docs: &[(Uri, Arc<ParsedDoc>)],
224) -> Vec<MethodStub> {
225 if let Some(fqn) = fqn {
226 for (_, doc) in all_docs {
229 if let Some(stubs) = collect_abstract_methods_fqn(&doc.program().stmts, fqn, "") {
230 return stubs;
231 }
232 }
233 return vec![];
234 }
235
236 for (_, doc) in all_docs {
238 if let Some(stubs) = collect_abstract_methods(&doc.program().stmts, name) {
239 return stubs;
240 }
241 }
242 vec![]
243}
244
245fn collect_abstract_methods_fqn(
249 stmts: &[Stmt<'_, '_>],
250 fqn: &str,
251 current_ns: &str,
252) -> Option<Vec<MethodStub>> {
253 let short = fqn_short_name(fqn);
255 let mut active_ns = current_ns.to_string();
258
259 for stmt in stmts {
260 match &stmt.kind {
261 StmtKind::Interface(i) if i.name == short => {
262 let declared_fqn = if active_ns.is_empty() {
264 i.name.to_string()
265 } else {
266 format!("{}\\{}", active_ns, i.name)
267 };
268 if fqn_eq(fqn, &declared_fqn) {
269 let stubs = i
270 .body
271 .members
272 .iter()
273 .filter_map(|m| {
274 if let ClassMemberKind::Method(method) = &m.kind {
275 Some(MethodStub {
276 name: method.name.to_string(),
277 visibility: "public",
278 is_static: method.is_static,
279 params: format_params_str(&method.params),
280 return_type: method
281 .return_type
282 .as_ref()
283 .map(|t| format_type_hint(t)),
284 })
285 } else {
286 None
287 }
288 })
289 .collect();
290 return Some(stubs);
291 }
292 }
293 StmtKind::Class(c)
294 if c.name.as_ref().map(|n| n.to_string()) == Some(short.to_string())
295 && c.modifiers.is_abstract =>
296 {
297 let declared_fqn = if active_ns.is_empty() {
298 short.to_string()
299 } else {
300 format!("{}\\{}", active_ns, short)
301 };
302 if fqn_eq(fqn, &declared_fqn) {
303 let stubs = c
304 .body
305 .members
306 .iter()
307 .filter_map(|m| {
308 if let ClassMemberKind::Method(method) = &m.kind {
309 if method.is_abstract {
310 Some(MethodStub {
311 name: method.name.to_string(),
312 visibility: visibility_str(method.visibility.as_ref()),
313 is_static: method.is_static,
314 params: format_params_str(&method.params),
315 return_type: method
316 .return_type
317 .as_ref()
318 .map(|t| format_type_hint(t)),
319 })
320 } else {
321 None
322 }
323 } else {
324 None
325 }
326 })
327 .collect();
328 return Some(stubs);
329 }
330 }
331 StmtKind::Namespace(ns) => {
332 let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().into_owned());
333 match &ns.body {
334 NamespaceBody::Braced(inner) => {
335 let child_ns = match &ns_name {
336 Some(n) if !active_ns.is_empty() => {
337 format!("{}\\{}", active_ns, n)
338 }
339 Some(n) => n.clone(),
340 None => active_ns.clone(),
341 };
342 if let Some(stubs) =
343 collect_abstract_methods_fqn(&inner.stmts, fqn, &child_ns)
344 {
345 return Some(stubs);
346 }
347 }
348 NamespaceBody::Simple => {
349 active_ns = ns_name.unwrap_or_default();
351 }
352 }
353 }
354 _ => {}
355 }
356 }
357 None
358}
359
360fn fqn_eq(a: &str, b: &str) -> bool {
362 a.trim_start_matches('\\') == b.trim_start_matches('\\')
363}
364
365fn collect_abstract_methods(stmts: &[Stmt<'_, '_>], name: &str) -> Option<Vec<MethodStub>> {
366 for stmt in stmts {
367 match &stmt.kind {
368 StmtKind::Interface(i) if i.name == name => {
369 let stubs = i
370 .body
371 .members
372 .iter()
373 .filter_map(|m| {
374 if let ClassMemberKind::Method(method) = &m.kind {
375 Some(MethodStub {
376 name: method.name.to_string(),
377 visibility: "public",
378 is_static: method.is_static,
379 params: format_params_str(&method.params),
380 return_type: method
381 .return_type
382 .as_ref()
383 .map(|t| format_type_hint(t)),
384 })
385 } else {
386 None
387 }
388 })
389 .collect();
390 return Some(stubs);
391 }
392 StmtKind::Class(c)
393 if c.name.as_ref().map(|n| n.to_string()) == Some(name.to_string())
394 && c.modifiers.is_abstract =>
395 {
396 let stubs = c
397 .body
398 .members
399 .iter()
400 .filter_map(|m| {
401 if let ClassMemberKind::Method(method) = &m.kind {
402 if method.is_abstract {
403 Some(MethodStub {
404 name: method.name.to_string(),
405 visibility: visibility_str(method.visibility.as_ref()),
406 is_static: method.is_static,
407 params: format_params_str(&method.params),
408 return_type: method
409 .return_type
410 .as_ref()
411 .map(|t| format_type_hint(t)),
412 })
413 } else {
414 None
415 }
416 } else {
417 None
418 }
419 })
420 .collect();
421 return Some(stubs);
422 }
423 StmtKind::Namespace(ns) => {
424 if let NamespaceBody::Braced(inner) = &ns.body
425 && let Some(stubs) = collect_abstract_methods(&inner.stmts, name)
426 {
427 return Some(stubs);
428 }
429 }
430 _ => {}
431 }
432 }
433 None
434}
435
436fn visibility_str(v: Option<&Visibility>) -> &'static str {
437 match v {
438 Some(Visibility::Protected) => "protected",
439 Some(Visibility::Private) => "private",
440 _ => "public",
441 }
442}
443
444fn generate_stub_text(stubs: &[MethodStub]) -> String {
445 let mut text = String::new();
446 for stub in stubs {
447 let static_kw = if stub.is_static { "static " } else { "" };
448 let ret = match &stub.return_type {
449 Some(t) => format!(": {t}"),
450 None => String::new(),
451 };
452 text.push_str(&format!(
453 " {} {}function {}({}){ret}\n {{\n throw new \\RuntimeException('Not implemented');\n }}\n\n",
454 stub.visibility, static_kw, stub.name, stub.params
455 ));
456 }
457 text
458}