Skip to main content

php_lsp/actions/
arrow_function_action.rs

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
13/// Offer "Convert to arrow function" when the cursor is inside a closure whose
14/// body is a single `return` statement and no use-by-ref captures are present.
15/// The innermost convertible closure at the cursor position wins.
16pub 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            // Prefer the innermost convertible closure: recurse into body first.
159            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            // Walk into the arrow function body so nested closures are reachable.
170            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    // Reject closures with by-ref `use` captures: arrow functions auto-capture
213    // by value and have no equivalent for `use (&$x)`.
214    if closure.use_vars.iter().any(|v| v.by_ref) {
215        return None;
216    }
217
218    // Body must be exactly one `return` statement with an expression.
219    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    // Generator closures (`yield`) cannot become arrow functions.
227    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
258/// Build the arrow-function source text for the given closure.
259///
260/// The params are extracted verbatim from source (preserving original
261/// formatting); the return type and body expression are sliced the same way.
262fn 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    // Find the opening `(` of the parameter list by scanning from the closure start.
271    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
295/// Find the index of the `)` that closes the `(` at `open`.
296/// Tracks paren depth to handle nested calls in default values.
297fn 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
315/// Return `true` if the expression is a `yield` — generator closures cannot
316/// be converted to arrow functions.
317fn is_yield(expr: &Expr<'_, '_>) -> bool {
318    matches!(expr.kind, ExprKind::Yield(_))
319}
320
321// ── Arrow function → closure ──────────────────────────────────────────────────
322
323/// Offer "Convert to closure" when the cursor is inside an arrow function.
324/// The innermost arrow function at the cursor position wins.
325pub 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            // Prefer the innermost arrow function: recurse into body first.
470            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            // Walk into closure body so nested arrow functions are reachable.
481            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
552/// Build the closure source text for the given arrow function.
553///
554/// Params and return type are extracted verbatim from source; the body
555/// expression is wrapped in `{ return <expr>; }`. Arrow functions
556/// auto-capture every outer-scope variable they read (by value — they can
557/// never write back into it), but a plain closure only sees what it
558/// declares in `use (...)`, so any such variable must be captured
559/// explicitly or the generated closure fails at runtime with an undefined
560/// variable.
561fn build_closure_text(af: &ArrowFunctionExpr<'_, '_>, span: Span, source: &str) -> Option<String> {
562    let af_start = span.start as usize;
563
564    // Find the opening `(` by scanning past the `fn` keyword (and optional `static `/`&`).
565    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
600/// Heuristic text scan for `$var` references, deduped in first-appearance
601/// order and excluding `$this` (always implicitly bound in a non-static
602/// closure, never valid in a `use` clause). Doesn't distinguish reads from
603/// writes or account for nested-closure scoping — matches the level of
604/// rigor `collect_vars_in_text` uses elsewhere in the action layer.
605fn 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                // `$this` is always implicitly bound; superglobals
620                // (`$_GET`, `$_SERVER`, `$GLOBALS`, ...) are part of the PHP
621                // runtime and are a compile-time error in a `use (...)`
622                // clause ("Cannot use $_GET as lexical variable as it is a
623                // superglobal") — neither belongs in the generated closure's
624                // capture list.
625                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}