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                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    // Reject closures with by-ref `use` captures: arrow functions auto-capture
216    // by value and have no equivalent for `use (&$x)`.
217    if closure.use_vars.iter().any(|v| v.by_ref) {
218        return None;
219    }
220
221    // Body must be exactly one `return` statement with an expression.
222    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    // Generator closures (`yield`) cannot become arrow functions.
230    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
261/// Build the arrow-function source text for the given closure.
262///
263/// The params are extracted verbatim from source (preserving original
264/// formatting); the return type and body expression are sliced the same way.
265fn 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    // Find the opening `(` of the parameter list by scanning from the closure start.
274    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
298/// Find the index of the `)` that closes the `(` at `open`.
299/// Tracks paren depth to handle nested calls in default values.
300fn 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
318/// Return `true` if the expression is a `yield` — generator closures cannot
319/// be converted to arrow functions.
320fn is_yield(expr: &Expr<'_, '_>) -> bool {
321    matches!(expr.kind, ExprKind::Yield(_))
322}
323
324// ── Arrow function → closure ──────────────────────────────────────────────────
325
326/// Offer "Convert to closure" when the cursor is inside an arrow function.
327/// The innermost arrow function at the cursor position wins.
328pub 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            // Prefer the innermost arrow function: recurse into body first.
473            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            // Walk into closure body so nested arrow functions are reachable.
484            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
558/// Build the closure source text for the given arrow function.
559///
560/// Params and return type are extracted verbatim from source; the body
561/// expression is wrapped in `{ return <expr>; }`. Arrow functions
562/// auto-capture every outer-scope variable they read (by value — they can
563/// never write back into it), but a plain closure only sees what it
564/// declares in `use (...)`, so any such variable must be captured
565/// explicitly or the generated closure fails at runtime with an undefined
566/// variable.
567fn build_closure_text(af: &ArrowFunctionExpr<'_, '_>, span: Span, source: &str) -> Option<String> {
568    let af_start = span.start as usize;
569
570    // Find the opening `(` by scanning past the `fn` keyword (and optional `static `/`&`).
571    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
606/// Heuristic text scan for `$var` references, deduped in first-appearance
607/// order and excluding `$this` (always implicitly bound in a non-static
608/// closure, never valid in a `use` clause). Doesn't distinguish reads from
609/// writes or account for nested-closure scoping — matches the level of
610/// rigor `collect_vars_in_text` uses elsewhere in the action layer.
611fn 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                // `$this` is always implicitly bound; superglobals
626                // (`$_GET`, `$_SERVER`, `$GLOBALS`, ...) are part of the PHP
627                // runtime and are a compile-time error in a `use (...)`
628                // clause ("Cannot use $_GET as lexical variable as it is a
629                // superglobal") — neither belongs in the generated closure's
630                // capture list.
631                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}