1use std::collections::HashMap;
2
3use php_ast::{
4 ArrowFunctionExpr, ClassMemberKind, ClosureExpr, Expr, ExprKind, NamespaceBody, Span, Stmt,
5 StmtKind,
6};
7use tower_lsp_server::ls_types::{
8 CodeAction, CodeActionKind, CodeActionOrCommand, Range, TextEdit, Uri, WorkspaceEdit,
9};
10
11use crate::document::ast::{ParsedDoc, SourceView};
12
13pub fn closure_to_arrow_function_actions(
17 source: &str,
18 doc: &ParsedDoc,
19 range: Range,
20 uri: &Uri,
21) -> Vec<CodeActionOrCommand> {
22 let sv = doc.view();
23 let cursor = sv.byte_of_position(range.start);
24 let mut out = Vec::new();
25 collect_in_stmts(&doc.program().stmts, source, cursor, uri, sv, &mut out);
26 out
27}
28
29fn collect_in_stmts(
30 stmts: &[Stmt<'_, '_>],
31 source: &str,
32 cursor: u32,
33 uri: &Uri,
34 sv: SourceView<'_>,
35 out: &mut Vec<CodeActionOrCommand>,
36) -> bool {
37 for stmt in stmts {
38 if stmt.span.end < cursor || stmt.span.start > cursor {
39 continue;
40 }
41 if collect_in_stmt(stmt, source, cursor, uri, sv, out) {
42 return true;
43 }
44 }
45 false
46}
47
48fn collect_in_stmt(
49 stmt: &Stmt<'_, '_>,
50 source: &str,
51 cursor: u32,
52 uri: &Uri,
53 sv: SourceView<'_>,
54 out: &mut Vec<CodeActionOrCommand>,
55) -> bool {
56 match &stmt.kind {
57 StmtKind::Expression(expr) | StmtKind::Throw(expr) => {
58 collect_in_expr(expr, source, cursor, uri, sv, out)
59 }
60 StmtKind::Return(Some(expr)) => collect_in_expr(expr, source, cursor, uri, sv, out),
61 StmtKind::Function(f) => collect_in_stmts(&f.body.stmts, source, cursor, uri, sv, out),
62 StmtKind::Class(c) => {
63 for member in c.body.members.iter() {
64 if let ClassMemberKind::Method(m) = &member.kind
65 && let Some(body) = &m.body
66 && collect_in_stmts(&body.stmts, source, cursor, uri, sv, out)
67 {
68 return true;
69 }
70 }
71 false
72 }
73 StmtKind::Namespace(ns) => {
74 if let NamespaceBody::Braced(inner) = &ns.body {
75 collect_in_stmts(&inner.stmts, source, cursor, uri, sv, out)
76 } else {
77 false
78 }
79 }
80 StmtKind::Block(b) => collect_in_stmts(&b.stmts, source, cursor, uri, sv, out),
81 StmtKind::If(i) => {
82 if collect_in_expr(&i.condition, source, cursor, uri, sv, out) {
83 return true;
84 }
85 if collect_in_stmt(i.then_branch, source, cursor, uri, sv, out) {
86 return true;
87 }
88 for ei in i.elseif_branches.iter() {
89 if collect_in_expr(&ei.condition, source, cursor, uri, sv, out)
90 || collect_in_stmt(&ei.body, source, cursor, uri, sv, out)
91 {
92 return true;
93 }
94 }
95 if let Some(e) = &i.else_branch {
96 collect_in_stmt(e, source, cursor, uri, sv, out)
97 } else {
98 false
99 }
100 }
101 StmtKind::While(w) => {
102 collect_in_expr(&w.condition, source, cursor, uri, sv, out)
103 || collect_in_stmt(w.body, source, cursor, uri, sv, out)
104 }
105 StmtKind::DoWhile(d) => {
106 collect_in_stmt(d.body, source, cursor, uri, sv, out)
107 || collect_in_expr(&d.condition, source, cursor, uri, sv, out)
108 }
109 StmtKind::For(f) => {
110 for e in f
111 .init
112 .iter()
113 .chain(f.condition.iter())
114 .chain(f.update.iter())
115 {
116 if collect_in_expr(e, source, cursor, uri, sv, out) {
117 return true;
118 }
119 }
120 collect_in_stmt(f.body, source, cursor, uri, sv, out)
121 }
122 StmtKind::Foreach(f) => {
123 collect_in_expr(&f.expr, source, cursor, uri, sv, out)
124 || collect_in_stmt(f.body, source, cursor, uri, sv, out)
125 }
126 StmtKind::TryCatch(t) => {
127 if collect_in_stmts(&t.body.stmts, source, cursor, uri, sv, out) {
128 return true;
129 }
130 for catch in t.catches.iter() {
131 if collect_in_stmts(&catch.body.stmts, source, cursor, uri, sv, out) {
132 return true;
133 }
134 }
135 if let Some(finally) = &t.finally {
136 collect_in_stmts(&finally.stmts, source, cursor, uri, sv, out)
137 } else {
138 false
139 }
140 }
141 _ => false,
142 }
143}
144
145fn collect_in_expr(
146 expr: &Expr<'_, '_>,
147 source: &str,
148 cursor: u32,
149 uri: &Uri,
150 sv: SourceView<'_>,
151 out: &mut Vec<CodeActionOrCommand>,
152) -> bool {
153 if expr.span.end < cursor || expr.span.start > cursor {
154 return false;
155 }
156 match &expr.kind {
157 ExprKind::Closure(c) => {
158 if collect_in_stmts(&c.body.stmts, source, cursor, uri, sv, out) {
160 return true;
161 }
162 if let Some(action) = build_action(c, expr.span, source, uri, sv) {
163 out.push(action);
164 return true;
165 }
166 false
167 }
168 ExprKind::ArrowFunction(af) => {
169 collect_in_expr(af.body, source, cursor, uri, sv, out)
171 }
172 ExprKind::Assign(a) => collect_in_expr(a.value, source, cursor, uri, sv, out),
173 ExprKind::FunctionCall(fc) => {
174 if collect_in_expr(fc.name, source, cursor, uri, sv, out) {
175 return true;
176 }
177 for arg in fc.args.iter() {
178 let Some(value) = &arg.value else { continue };
179 if collect_in_expr(value, source, cursor, uri, sv, out) {
180 return true;
181 }
182 }
183 false
184 }
185 ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
186 for arg in mc.args.iter() {
187 let Some(value) = &arg.value else { continue };
188 if collect_in_expr(value, source, cursor, uri, sv, out) {
189 return true;
190 }
191 }
192 false
193 }
194 ExprKind::StaticMethodCall(smc) => {
195 for arg in smc.args.iter() {
196 let Some(value) = &arg.value else { continue };
197 if collect_in_expr(value, source, cursor, uri, sv, out) {
198 return true;
199 }
200 }
201 false
202 }
203 ExprKind::Parenthesized(inner) => collect_in_expr(inner, source, cursor, uri, sv, out),
204 _ => false,
205 }
206}
207
208fn build_action(
209 closure: &ClosureExpr<'_, '_>,
210 span: Span,
211 source: &str,
212 uri: &Uri,
213 sv: SourceView<'_>,
214) -> Option<CodeActionOrCommand> {
215 if closure.use_vars.iter().any(|v| v.by_ref) {
218 return None;
219 }
220
221 if closure.body.stmts.len() != 1 {
223 return None;
224 }
225 let StmtKind::Return(Some(return_expr)) = &closure.body.stmts[0].kind else {
226 return None;
227 };
228
229 if is_yield(return_expr) {
231 return None;
232 }
233
234 let new_text = build_arrow_text(closure, span, return_expr, source)?;
235
236 let edit_range = Range {
237 start: sv.position_of(span.start),
238 end: sv.position_of(span.end),
239 };
240
241 let mut changes = HashMap::new();
242 changes.insert(
243 uri.clone(),
244 vec![TextEdit {
245 range: edit_range,
246 new_text,
247 }],
248 );
249
250 Some(CodeActionOrCommand::CodeAction(CodeAction {
251 title: "Convert to arrow function".to_string(),
252 kind: Some(CodeActionKind::REFACTOR),
253 edit: Some(WorkspaceEdit {
254 changes: Some(changes),
255 ..Default::default()
256 }),
257 ..Default::default()
258 }))
259}
260
261fn build_arrow_text(
266 closure: &ClosureExpr<'_, '_>,
267 span: Span,
268 return_expr: &Expr<'_, '_>,
269 source: &str,
270) -> Option<String> {
271 let closure_start = span.start as usize;
272
273 let open_paren = source[closure_start..].find('(')? + closure_start;
275 let close_paren = find_close_paren(source.as_bytes(), open_paren)?;
276
277 let params_text = &source[open_paren..=close_paren];
278
279 let ret_text = if let Some(rt) = &closure.return_type {
280 format!(
281 ": {}",
282 &source[rt.span.start as usize..rt.span.end as usize]
283 )
284 } else {
285 String::new()
286 };
287
288 let expr_text = &source[return_expr.span.start as usize..return_expr.span.end as usize];
289
290 let static_prefix = if closure.is_static { "static " } else { "" };
291 let by_ref_marker = if closure.by_ref { "&" } else { "" };
292
293 Some(format!(
294 "{static_prefix}fn{by_ref_marker}{params_text}{ret_text} => {expr_text}"
295 ))
296}
297
298fn find_close_paren(source: &[u8], open: usize) -> Option<usize> {
301 debug_assert_eq!(source[open], b'(');
302 let mut depth = 0i32;
303 for (i, &b) in source[open..].iter().enumerate() {
304 match b {
305 b'(' => depth += 1,
306 b')' => {
307 depth -= 1;
308 if depth == 0 {
309 return Some(open + i);
310 }
311 }
312 _ => {}
313 }
314 }
315 None
316}
317
318fn is_yield(expr: &Expr<'_, '_>) -> bool {
321 matches!(expr.kind, ExprKind::Yield(_))
322}
323
324pub fn arrow_function_to_closure_actions(
329 source: &str,
330 doc: &ParsedDoc,
331 range: Range,
332 uri: &Uri,
333) -> Vec<CodeActionOrCommand> {
334 let sv = doc.view();
335 let cursor = sv.byte_of_position(range.start);
336 let mut out = Vec::new();
337 collect_arrow_in_stmts(&doc.program().stmts, source, cursor, uri, sv, &mut out);
338 out
339}
340
341fn collect_arrow_in_stmts(
342 stmts: &[Stmt<'_, '_>],
343 source: &str,
344 cursor: u32,
345 uri: &Uri,
346 sv: SourceView<'_>,
347 out: &mut Vec<CodeActionOrCommand>,
348) -> bool {
349 for stmt in stmts {
350 if stmt.span.end < cursor || stmt.span.start > cursor {
351 continue;
352 }
353 if collect_arrow_in_stmt(stmt, source, cursor, uri, sv, out) {
354 return true;
355 }
356 }
357 false
358}
359
360fn collect_arrow_in_stmt(
361 stmt: &Stmt<'_, '_>,
362 source: &str,
363 cursor: u32,
364 uri: &Uri,
365 sv: SourceView<'_>,
366 out: &mut Vec<CodeActionOrCommand>,
367) -> bool {
368 match &stmt.kind {
369 StmtKind::Expression(expr) | StmtKind::Throw(expr) => {
370 collect_arrow_in_expr(expr, source, cursor, uri, sv, out)
371 }
372 StmtKind::Return(Some(expr)) => collect_arrow_in_expr(expr, source, cursor, uri, sv, out),
373 StmtKind::Function(f) => {
374 collect_arrow_in_stmts(&f.body.stmts, source, cursor, uri, sv, out)
375 }
376 StmtKind::Class(c) => {
377 for member in c.body.members.iter() {
378 if let ClassMemberKind::Method(m) = &member.kind
379 && let Some(body) = &m.body
380 && collect_arrow_in_stmts(&body.stmts, source, cursor, uri, sv, out)
381 {
382 return true;
383 }
384 }
385 false
386 }
387 StmtKind::Namespace(ns) => {
388 if let NamespaceBody::Braced(inner) = &ns.body {
389 collect_arrow_in_stmts(&inner.stmts, source, cursor, uri, sv, out)
390 } else {
391 false
392 }
393 }
394 StmtKind::Block(b) => collect_arrow_in_stmts(&b.stmts, source, cursor, uri, sv, out),
395 StmtKind::If(i) => {
396 if collect_arrow_in_expr(&i.condition, source, cursor, uri, sv, out) {
397 return true;
398 }
399 if collect_arrow_in_stmt(i.then_branch, source, cursor, uri, sv, out) {
400 return true;
401 }
402 for ei in i.elseif_branches.iter() {
403 if collect_arrow_in_expr(&ei.condition, source, cursor, uri, sv, out)
404 || collect_arrow_in_stmt(&ei.body, source, cursor, uri, sv, out)
405 {
406 return true;
407 }
408 }
409 if let Some(e) = &i.else_branch {
410 collect_arrow_in_stmt(e, source, cursor, uri, sv, out)
411 } else {
412 false
413 }
414 }
415 StmtKind::While(w) => {
416 collect_arrow_in_expr(&w.condition, source, cursor, uri, sv, out)
417 || collect_arrow_in_stmt(w.body, source, cursor, uri, sv, out)
418 }
419 StmtKind::DoWhile(d) => {
420 collect_arrow_in_stmt(d.body, source, cursor, uri, sv, out)
421 || collect_arrow_in_expr(&d.condition, source, cursor, uri, sv, out)
422 }
423 StmtKind::For(f) => {
424 for e in f
425 .init
426 .iter()
427 .chain(f.condition.iter())
428 .chain(f.update.iter())
429 {
430 if collect_arrow_in_expr(e, source, cursor, uri, sv, out) {
431 return true;
432 }
433 }
434 collect_arrow_in_stmt(f.body, source, cursor, uri, sv, out)
435 }
436 StmtKind::Foreach(f) => {
437 collect_arrow_in_expr(&f.expr, source, cursor, uri, sv, out)
438 || collect_arrow_in_stmt(f.body, source, cursor, uri, sv, out)
439 }
440 StmtKind::TryCatch(t) => {
441 if collect_arrow_in_stmts(&t.body.stmts, source, cursor, uri, sv, out) {
442 return true;
443 }
444 for catch in t.catches.iter() {
445 if collect_arrow_in_stmts(&catch.body.stmts, source, cursor, uri, sv, out) {
446 return true;
447 }
448 }
449 if let Some(finally) = &t.finally {
450 collect_arrow_in_stmts(&finally.stmts, source, cursor, uri, sv, out)
451 } else {
452 false
453 }
454 }
455 _ => false,
456 }
457}
458
459fn collect_arrow_in_expr(
460 expr: &Expr<'_, '_>,
461 source: &str,
462 cursor: u32,
463 uri: &Uri,
464 sv: SourceView<'_>,
465 out: &mut Vec<CodeActionOrCommand>,
466) -> bool {
467 if expr.span.end < cursor || expr.span.start > cursor {
468 return false;
469 }
470 match &expr.kind {
471 ExprKind::ArrowFunction(af) => {
472 if collect_arrow_in_expr(af.body, source, cursor, uri, sv, out) {
474 return true;
475 }
476 if let Some(action) = build_arrow_to_closure_action(af, expr.span, source, uri, sv) {
477 out.push(action);
478 return true;
479 }
480 false
481 }
482 ExprKind::Closure(c) => {
483 collect_arrow_in_stmts(&c.body.stmts, source, cursor, uri, sv, out)
485 }
486 ExprKind::Assign(a) => collect_arrow_in_expr(a.value, source, cursor, uri, sv, out),
487 ExprKind::FunctionCall(fc) => {
488 if collect_arrow_in_expr(fc.name, source, cursor, uri, sv, out) {
489 return true;
490 }
491 for arg in fc.args.iter() {
492 let Some(value) = &arg.value else { continue };
493 if collect_arrow_in_expr(value, source, cursor, uri, sv, out) {
494 return true;
495 }
496 }
497 false
498 }
499 ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
500 for arg in mc.args.iter() {
501 let Some(value) = &arg.value else { continue };
502 if collect_arrow_in_expr(value, source, cursor, uri, sv, out) {
503 return true;
504 }
505 }
506 false
507 }
508 ExprKind::StaticMethodCall(smc) => {
509 for arg in smc.args.iter() {
510 let Some(value) = &arg.value else { continue };
511 if collect_arrow_in_expr(value, source, cursor, uri, sv, out) {
512 return true;
513 }
514 }
515 false
516 }
517 ExprKind::Parenthesized(inner) => {
518 collect_arrow_in_expr(inner, source, cursor, uri, sv, out)
519 }
520 _ => false,
521 }
522}
523
524fn build_arrow_to_closure_action(
525 af: &ArrowFunctionExpr<'_, '_>,
526 span: Span,
527 source: &str,
528 uri: &Uri,
529 sv: SourceView<'_>,
530) -> Option<CodeActionOrCommand> {
531 let new_text = build_closure_text(af, span, source)?;
532
533 let edit_range = Range {
534 start: sv.position_of(span.start),
535 end: sv.position_of(span.end),
536 };
537
538 let mut changes = HashMap::new();
539 changes.insert(
540 uri.clone(),
541 vec![TextEdit {
542 range: edit_range,
543 new_text,
544 }],
545 );
546
547 Some(CodeActionOrCommand::CodeAction(CodeAction {
548 title: "Convert to closure".to_string(),
549 kind: Some(CodeActionKind::REFACTOR),
550 edit: Some(WorkspaceEdit {
551 changes: Some(changes),
552 ..Default::default()
553 }),
554 ..Default::default()
555 }))
556}
557
558fn build_closure_text(af: &ArrowFunctionExpr<'_, '_>, span: Span, source: &str) -> Option<String> {
568 let af_start = span.start as usize;
569
570 let open_paren = source[af_start..].find('(')? + af_start;
572 let close_paren = find_close_paren(source.as_bytes(), open_paren)?;
573
574 let params_text = &source[open_paren..=close_paren];
575
576 let ret_text = if let Some(rt) = &af.return_type {
577 format!(
578 ": {}",
579 &source[rt.span.start as usize..rt.span.end as usize]
580 )
581 } else {
582 String::new()
583 };
584
585 let body_text = &source[af.body.span.start as usize..af.body.span.end as usize];
586
587 let static_prefix = if af.is_static { "static " } else { "" };
588 let by_ref_marker = if af.by_ref { "&" } else { "" };
589
590 let param_names = collect_vars(params_text);
591 let captured = collect_vars(body_text)
592 .into_iter()
593 .filter(|v| !param_names.contains(v))
594 .collect::<Vec<_>>();
595 let use_clause = if captured.is_empty() {
596 String::new()
597 } else {
598 format!(" use ({})", captured.join(", "))
599 };
600
601 Some(format!(
602 "{static_prefix}function{by_ref_marker}{params_text}{use_clause}{ret_text} {{ return {body_text}; }}"
603 ))
604}
605
606fn collect_vars(text: &str) -> Vec<String> {
612 let mut vars: Vec<String> = Vec::new();
613 let bytes = text.as_bytes();
614 let mut i = 0;
615 while i < bytes.len() {
616 if bytes[i] == b'$' {
617 let start = i + 1;
618 let mut end = start;
619 while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') {
620 end += 1;
621 }
622 if end > start {
623 let name = &text[start..end];
624 let full = format!("${name}");
625 if name != "this"
632 && !crate::editing::rename::is_superglobal(&full)
633 && !vars.contains(&full)
634 {
635 vars.push(full);
636 }
637 }
638 i = end;
639 } else {
640 i += 1;
641 }
642 }
643 vars
644}