Skip to main content

php_lsp/actions/
phpdoc_action.rs

1/// Code action: generate a PHPDoc stub for a function or method that lacks one.
2use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Param, Stmt, StmtKind};
3use tower_lsp::lsp_types::{
4    CodeAction, CodeActionKind, CodeActionOrCommand, Position, Range, TextEdit, Url, WorkspaceEdit,
5};
6
7use crate::document::ast::{ParsedDoc, SourceView, format_type_hint};
8
9/// Return "Generate PHPDoc" code actions for any function/method whose declaration line
10/// falls within `range` and does not already have a docblock.
11pub fn phpdoc_actions(
12    uri: &Url,
13    doc: &ParsedDoc,
14    _source: &str,
15    range: Range,
16) -> Vec<CodeActionOrCommand> {
17    let sv = doc.view();
18    let mut actions = Vec::new();
19    collect(&doc.program().stmts, uri, sv, range, &mut actions);
20    actions
21}
22
23fn collect(
24    stmts: &[Stmt<'_, '_>],
25    uri: &Url,
26    sv: SourceView<'_>,
27    range: Range,
28    out: &mut Vec<CodeActionOrCommand>,
29) {
30    for stmt in stmts {
31        match &stmt.kind {
32            StmtKind::Function(f) => {
33                let fn_line = sv.position_of(stmt.span.start).line;
34                if line_in_range(fn_line, range) && f.doc_comment.is_none() {
35                    let ret = f.return_type.as_ref().map(|t| format_type_hint(t));
36                    if let Some(action) = make_action(uri, sv.source(), fn_line, &f.params, ret) {
37                        out.push(action);
38                    }
39                }
40            }
41            StmtKind::Class(c) => {
42                for member in c.body.members.iter() {
43                    if let ClassMemberKind::Method(m) = &member.kind {
44                        let fn_line = sv.position_of(member.span.start).line;
45                        if line_in_range(fn_line, range) && m.doc_comment.is_none() {
46                            let ret = m.return_type.as_ref().map(|t| format_type_hint(t));
47                            if let Some(action) =
48                                make_action(uri, sv.source(), fn_line, &m.params, ret)
49                            {
50                                out.push(action);
51                            }
52                        }
53                    }
54                }
55            }
56            StmtKind::Trait(t) => {
57                for member in t.body.members.iter() {
58                    if let ClassMemberKind::Method(m) = &member.kind {
59                        let fn_line = sv.position_of(member.span.start).line;
60                        if line_in_range(fn_line, range) && m.doc_comment.is_none() {
61                            let ret = m.return_type.as_ref().map(|t| format_type_hint(t));
62                            if let Some(action) =
63                                make_action(uri, sv.source(), fn_line, &m.params, ret)
64                            {
65                                out.push(action);
66                            }
67                        }
68                    }
69                }
70            }
71            StmtKind::Enum(e) => {
72                for member in e.body.members.iter() {
73                    if let EnumMemberKind::Method(m) = &member.kind {
74                        let fn_line = sv.position_of(member.span.start).line;
75                        if line_in_range(fn_line, range) && m.doc_comment.is_none() {
76                            let ret = m.return_type.as_ref().map(|t| format_type_hint(t));
77                            if let Some(action) =
78                                make_action(uri, sv.source(), fn_line, &m.params, ret)
79                            {
80                                out.push(action);
81                            }
82                        }
83                    }
84                }
85            }
86            StmtKind::Namespace(ns) => {
87                if let NamespaceBody::Braced(inner) = &ns.body {
88                    collect(&inner.stmts, uri, sv, range, out);
89                }
90            }
91            _ => {}
92        }
93    }
94}
95
96fn line_in_range(line: u32, range: Range) -> bool {
97    line >= range.start.line && line <= range.end.line
98}
99
100fn make_action(
101    uri: &Url,
102    source: &str,
103    fn_line: u32,
104    params: &[Param<'_, '_>],
105    return_type: Option<String>,
106) -> Option<CodeActionOrCommand> {
107    let indent = source
108        .lines()
109        .nth(fn_line as usize)
110        .map(|line| {
111            let n = line.len() - line.trim_start().len();
112            &line[..n]
113        })
114        .unwrap_or("")
115        .to_string();
116
117    let mut lines: Vec<String> = vec![format!("{indent}/**")];
118
119    for p in params.iter() {
120        let type_part = p
121            .type_hint
122            .as_ref()
123            .map(|t| format!("{} ", format_type_hint(t)))
124            .unwrap_or_default();
125        let name = format!("${}", p.name);
126        lines.push(format!("{indent} * @param {type_part}{name}"));
127    }
128
129    if let Some(ret) = return_type {
130        lines.push(format!("{indent} * @return {ret}"));
131    }
132
133    lines.push(format!("{indent} */"));
134
135    let new_text = lines.join("\n") + "\n";
136    let pos = Position {
137        line: fn_line,
138        character: 0,
139    };
140    let edit = TextEdit {
141        range: Range {
142            start: pos,
143            end: pos,
144        },
145        new_text,
146    };
147
148    let mut changes = std::collections::HashMap::new();
149    changes.insert(uri.clone(), vec![edit]);
150
151    Some(CodeActionOrCommand::CodeAction(CodeAction {
152        title: "Generate PHPDoc".to_string(),
153        kind: Some(CodeActionKind::REFACTOR),
154        edit: Some(WorkspaceEdit {
155            changes: Some(changes),
156            ..Default::default()
157        }),
158        ..Default::default()
159    }))
160}