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