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 if collect_in_expr(&arg.value, source, cursor, uri, sv, out) {
179 return true;
180 }
181 }
182 false
183 }
184 ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
185 for arg in mc.args.iter() {
186 if collect_in_expr(&arg.value, source, cursor, uri, sv, out) {
187 return true;
188 }
189 }
190 false
191 }
192 ExprKind::StaticMethodCall(smc) => {
193 for arg in smc.args.iter() {
194 if collect_in_expr(&arg.value, source, cursor, uri, sv, out) {
195 return true;
196 }
197 }
198 false
199 }
200 ExprKind::Parenthesized(inner) => collect_in_expr(inner, source, cursor, uri, sv, out),
201 _ => false,
202 }
203}
204
205fn build_action(
206 closure: &ClosureExpr<'_, '_>,
207 span: Span,
208 source: &str,
209 uri: &Uri,
210 sv: SourceView<'_>,
211) -> Option<CodeActionOrCommand> {
212 if closure.use_vars.iter().any(|v| v.by_ref) {
215 return None;
216 }
217
218 if closure.body.stmts.len() != 1 {
220 return None;
221 }
222 let StmtKind::Return(Some(return_expr)) = &closure.body.stmts[0].kind else {
223 return None;
224 };
225
226 if is_yield(return_expr) {
228 return None;
229 }
230
231 let new_text = build_arrow_text(closure, span, return_expr, source)?;
232
233 let edit_range = Range {
234 start: sv.position_of(span.start),
235 end: sv.position_of(span.end),
236 };
237
238 let mut changes = HashMap::new();
239 changes.insert(
240 uri.clone(),
241 vec![TextEdit {
242 range: edit_range,
243 new_text,
244 }],
245 );
246
247 Some(CodeActionOrCommand::CodeAction(CodeAction {
248 title: "Convert to arrow function".to_string(),
249 kind: Some(CodeActionKind::REFACTOR),
250 edit: Some(WorkspaceEdit {
251 changes: Some(changes),
252 ..Default::default()
253 }),
254 ..Default::default()
255 }))
256}
257
258fn build_arrow_text(
263 closure: &ClosureExpr<'_, '_>,
264 span: Span,
265 return_expr: &Expr<'_, '_>,
266 source: &str,
267) -> Option<String> {
268 let closure_start = span.start as usize;
269
270 let open_paren = source[closure_start..].find('(')? + closure_start;
272 let close_paren = find_close_paren(source.as_bytes(), open_paren)?;
273
274 let params_text = &source[open_paren..=close_paren];
275
276 let ret_text = if let Some(rt) = &closure.return_type {
277 format!(
278 ": {}",
279 &source[rt.span.start as usize..rt.span.end as usize]
280 )
281 } else {
282 String::new()
283 };
284
285 let expr_text = &source[return_expr.span.start as usize..return_expr.span.end as usize];
286
287 let static_prefix = if closure.is_static { "static " } else { "" };
288 let by_ref_marker = if closure.by_ref { "&" } else { "" };
289
290 Some(format!(
291 "{static_prefix}fn{by_ref_marker}{params_text}{ret_text} => {expr_text}"
292 ))
293}
294
295fn find_close_paren(source: &[u8], open: usize) -> Option<usize> {
298 debug_assert_eq!(source[open], b'(');
299 let mut depth = 0i32;
300 for (i, &b) in source[open..].iter().enumerate() {
301 match b {
302 b'(' => depth += 1,
303 b')' => {
304 depth -= 1;
305 if depth == 0 {
306 return Some(open + i);
307 }
308 }
309 _ => {}
310 }
311 }
312 None
313}
314
315fn is_yield(expr: &Expr<'_, '_>) -> bool {
318 matches!(expr.kind, ExprKind::Yield(_))
319}
320
321pub fn arrow_function_to_closure_actions(
326 source: &str,
327 doc: &ParsedDoc,
328 range: Range,
329 uri: &Uri,
330) -> Vec<CodeActionOrCommand> {
331 let sv = doc.view();
332 let cursor = sv.byte_of_position(range.start);
333 let mut out = Vec::new();
334 collect_arrow_in_stmts(&doc.program().stmts, source, cursor, uri, sv, &mut out);
335 out
336}
337
338fn collect_arrow_in_stmts(
339 stmts: &[Stmt<'_, '_>],
340 source: &str,
341 cursor: u32,
342 uri: &Uri,
343 sv: SourceView<'_>,
344 out: &mut Vec<CodeActionOrCommand>,
345) -> bool {
346 for stmt in stmts {
347 if stmt.span.end < cursor || stmt.span.start > cursor {
348 continue;
349 }
350 if collect_arrow_in_stmt(stmt, source, cursor, uri, sv, out) {
351 return true;
352 }
353 }
354 false
355}
356
357fn collect_arrow_in_stmt(
358 stmt: &Stmt<'_, '_>,
359 source: &str,
360 cursor: u32,
361 uri: &Uri,
362 sv: SourceView<'_>,
363 out: &mut Vec<CodeActionOrCommand>,
364) -> bool {
365 match &stmt.kind {
366 StmtKind::Expression(expr) | StmtKind::Throw(expr) => {
367 collect_arrow_in_expr(expr, source, cursor, uri, sv, out)
368 }
369 StmtKind::Return(Some(expr)) => collect_arrow_in_expr(expr, source, cursor, uri, sv, out),
370 StmtKind::Function(f) => {
371 collect_arrow_in_stmts(&f.body.stmts, source, cursor, uri, sv, out)
372 }
373 StmtKind::Class(c) => {
374 for member in c.body.members.iter() {
375 if let ClassMemberKind::Method(m) = &member.kind
376 && let Some(body) = &m.body
377 && collect_arrow_in_stmts(&body.stmts, source, cursor, uri, sv, out)
378 {
379 return true;
380 }
381 }
382 false
383 }
384 StmtKind::Namespace(ns) => {
385 if let NamespaceBody::Braced(inner) = &ns.body {
386 collect_arrow_in_stmts(&inner.stmts, source, cursor, uri, sv, out)
387 } else {
388 false
389 }
390 }
391 StmtKind::Block(b) => collect_arrow_in_stmts(&b.stmts, source, cursor, uri, sv, out),
392 StmtKind::If(i) => {
393 if collect_arrow_in_expr(&i.condition, source, cursor, uri, sv, out) {
394 return true;
395 }
396 if collect_arrow_in_stmt(i.then_branch, source, cursor, uri, sv, out) {
397 return true;
398 }
399 for ei in i.elseif_branches.iter() {
400 if collect_arrow_in_expr(&ei.condition, source, cursor, uri, sv, out)
401 || collect_arrow_in_stmt(&ei.body, source, cursor, uri, sv, out)
402 {
403 return true;
404 }
405 }
406 if let Some(e) = &i.else_branch {
407 collect_arrow_in_stmt(e, source, cursor, uri, sv, out)
408 } else {
409 false
410 }
411 }
412 StmtKind::While(w) => {
413 collect_arrow_in_expr(&w.condition, source, cursor, uri, sv, out)
414 || collect_arrow_in_stmt(w.body, source, cursor, uri, sv, out)
415 }
416 StmtKind::DoWhile(d) => {
417 collect_arrow_in_stmt(d.body, source, cursor, uri, sv, out)
418 || collect_arrow_in_expr(&d.condition, source, cursor, uri, sv, out)
419 }
420 StmtKind::For(f) => {
421 for e in f
422 .init
423 .iter()
424 .chain(f.condition.iter())
425 .chain(f.update.iter())
426 {
427 if collect_arrow_in_expr(e, source, cursor, uri, sv, out) {
428 return true;
429 }
430 }
431 collect_arrow_in_stmt(f.body, source, cursor, uri, sv, out)
432 }
433 StmtKind::Foreach(f) => {
434 collect_arrow_in_expr(&f.expr, source, cursor, uri, sv, out)
435 || collect_arrow_in_stmt(f.body, source, cursor, uri, sv, out)
436 }
437 StmtKind::TryCatch(t) => {
438 if collect_arrow_in_stmts(&t.body.stmts, source, cursor, uri, sv, out) {
439 return true;
440 }
441 for catch in t.catches.iter() {
442 if collect_arrow_in_stmts(&catch.body.stmts, source, cursor, uri, sv, out) {
443 return true;
444 }
445 }
446 if let Some(finally) = &t.finally {
447 collect_arrow_in_stmts(&finally.stmts, source, cursor, uri, sv, out)
448 } else {
449 false
450 }
451 }
452 _ => false,
453 }
454}
455
456fn collect_arrow_in_expr(
457 expr: &Expr<'_, '_>,
458 source: &str,
459 cursor: u32,
460 uri: &Uri,
461 sv: SourceView<'_>,
462 out: &mut Vec<CodeActionOrCommand>,
463) -> bool {
464 if expr.span.end < cursor || expr.span.start > cursor {
465 return false;
466 }
467 match &expr.kind {
468 ExprKind::ArrowFunction(af) => {
469 if collect_arrow_in_expr(af.body, source, cursor, uri, sv, out) {
471 return true;
472 }
473 if let Some(action) = build_arrow_to_closure_action(af, expr.span, source, uri, sv) {
474 out.push(action);
475 return true;
476 }
477 false
478 }
479 ExprKind::Closure(c) => {
480 collect_arrow_in_stmts(&c.body.stmts, source, cursor, uri, sv, out)
482 }
483 ExprKind::Assign(a) => collect_arrow_in_expr(a.value, source, cursor, uri, sv, out),
484 ExprKind::FunctionCall(fc) => {
485 if collect_arrow_in_expr(fc.name, source, cursor, uri, sv, out) {
486 return true;
487 }
488 for arg in fc.args.iter() {
489 if collect_arrow_in_expr(&arg.value, source, cursor, uri, sv, out) {
490 return true;
491 }
492 }
493 false
494 }
495 ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
496 for arg in mc.args.iter() {
497 if collect_arrow_in_expr(&arg.value, source, cursor, uri, sv, out) {
498 return true;
499 }
500 }
501 false
502 }
503 ExprKind::StaticMethodCall(smc) => {
504 for arg in smc.args.iter() {
505 if collect_arrow_in_expr(&arg.value, source, cursor, uri, sv, out) {
506 return true;
507 }
508 }
509 false
510 }
511 ExprKind::Parenthesized(inner) => {
512 collect_arrow_in_expr(inner, source, cursor, uri, sv, out)
513 }
514 _ => false,
515 }
516}
517
518fn build_arrow_to_closure_action(
519 af: &ArrowFunctionExpr<'_, '_>,
520 span: Span,
521 source: &str,
522 uri: &Uri,
523 sv: SourceView<'_>,
524) -> Option<CodeActionOrCommand> {
525 let new_text = build_closure_text(af, span, source)?;
526
527 let edit_range = Range {
528 start: sv.position_of(span.start),
529 end: sv.position_of(span.end),
530 };
531
532 let mut changes = HashMap::new();
533 changes.insert(
534 uri.clone(),
535 vec![TextEdit {
536 range: edit_range,
537 new_text,
538 }],
539 );
540
541 Some(CodeActionOrCommand::CodeAction(CodeAction {
542 title: "Convert to closure".to_string(),
543 kind: Some(CodeActionKind::REFACTOR),
544 edit: Some(WorkspaceEdit {
545 changes: Some(changes),
546 ..Default::default()
547 }),
548 ..Default::default()
549 }))
550}
551
552fn build_closure_text(af: &ArrowFunctionExpr<'_, '_>, span: Span, source: &str) -> Option<String> {
562 let af_start = span.start as usize;
563
564 let open_paren = source[af_start..].find('(')? + af_start;
566 let close_paren = find_close_paren(source.as_bytes(), open_paren)?;
567
568 let params_text = &source[open_paren..=close_paren];
569
570 let ret_text = if let Some(rt) = &af.return_type {
571 format!(
572 ": {}",
573 &source[rt.span.start as usize..rt.span.end as usize]
574 )
575 } else {
576 String::new()
577 };
578
579 let body_text = &source[af.body.span.start as usize..af.body.span.end as usize];
580
581 let static_prefix = if af.is_static { "static " } else { "" };
582 let by_ref_marker = if af.by_ref { "&" } else { "" };
583
584 let param_names = collect_vars(params_text);
585 let captured = collect_vars(body_text)
586 .into_iter()
587 .filter(|v| !param_names.contains(v))
588 .collect::<Vec<_>>();
589 let use_clause = if captured.is_empty() {
590 String::new()
591 } else {
592 format!(" use ({})", captured.join(", "))
593 };
594
595 Some(format!(
596 "{static_prefix}function{by_ref_marker}{params_text}{use_clause}{ret_text} {{ return {body_text}; }}"
597 ))
598}
599
600fn collect_vars(text: &str) -> Vec<String> {
606 let mut vars: Vec<String> = Vec::new();
607 let bytes = text.as_bytes();
608 let mut i = 0;
609 while i < bytes.len() {
610 if bytes[i] == b'$' {
611 let start = i + 1;
612 let mut end = start;
613 while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') {
614 end += 1;
615 }
616 if end > start {
617 let name = &text[start..end];
618 let full = format!("${name}");
619 if name != "this"
626 && !crate::editing::rename::is_superglobal(&full)
627 && !vars.contains(&full)
628 {
629 vars.push(full);
630 }
631 }
632 i = end;
633 } else {
634 i += 1;
635 }
636 }
637 vars
638}