Skip to main content

php_lsp/navigation/
declaration.rs

1/// `textDocument/declaration` — jump to the abstract or interface declaration of a symbol.
2///
3/// In PHP the distinction between declaration and definition matters for:
4///   - Interface methods (declared but never given a body)
5///   - Abstract class methods
6///
7/// For concrete symbols with no abstract counterpart this falls back to the same
8/// result as go-to-definition so the request is never empty-handed.
9use std::sync::Arc;
10
11use tower_lsp::lsp_types::{Location, Position, Url};
12
13use crate::document::ast::ParsedDoc;
14use crate::text::{strip_variable_sigil, utf16_code_units, word_at_position};
15use crate::types::resolve::{Container, Declaration, resolve_declaration};
16
17/// Find the abstract or interface declaration of `word`.
18/// Prefers abstract/interface declarations; falls back to any declaration.
19pub fn goto_declaration(
20    source: &str,
21    all_docs: &[(Url, Arc<ParsedDoc>)],
22    position: Position,
23) -> Option<Location> {
24    let word = word_at_position(source, position)?;
25
26    // First pass: look for an abstract or interface declaration
27    for (uri, doc) in all_docs {
28        let sv = doc.view();
29        if let Some(decl) =
30            resolve_declaration(&doc.program().stmts, &word, &is_abstract_declaration)
31        {
32            return Some(Location {
33                uri: uri.clone(),
34                range: sv.name_range_in_span(decl.name(), decl.span()),
35            });
36        }
37    }
38
39    // Second pass: any declaration (same as goto_definition)
40    for (uri, doc) in all_docs {
41        let sv = doc.view();
42        if let Some(decl) = resolve_declaration(&doc.program().stmts, &word, &is_any_declaration) {
43            return Some(Location {
44                uri: uri.clone(),
45                range: sv.name_range_in_span(decl.name(), decl.span()),
46            });
47        }
48    }
49
50    None
51}
52
53/// Pass 1: abstract/interface declarations only — interface members and names,
54/// plus abstract methods on classes and traits.
55///
56/// `resolve_declaration` checks a type's name before its members, whereas the original
57/// walker checked interface members first. This only differs for an interface
58/// named the same as a method it contains — syntactically legal but absurd, and
59/// never seen in real PHP, so the order is harmless here.
60fn is_abstract_declaration(decl: &Declaration<'_>) -> bool {
61    match decl {
62        Declaration::Interface { .. } => true,
63        Declaration::Method {
64            container: Container::Interface,
65            ..
66        } => true,
67        Declaration::Method {
68            method,
69            container: Container::Class | Container::Trait,
70            ..
71        } => method.is_abstract,
72        _ => false,
73    }
74}
75
76/// Pass 2: any declaration. Constructor-promoted parameters are not surfaced as
77/// declarations here (the original `find_any_declaration` never matched them).
78fn is_any_declaration(decl: &Declaration<'_>) -> bool {
79    !matches!(decl, Declaration::PromotedParam { .. })
80}
81
82/// Find abstract or interface declaration using `FileIndex` entries.
83/// Returns line-only positions (character 0) for unopened files.
84/// This is a limitation of the compact FileIndex — for opened files,
85/// goto_declaration() provides precise name ranges.
86pub fn goto_declaration_from_index(
87    source: &str,
88    indexes: &[(
89        tower_lsp::lsp_types::Url,
90        std::sync::Arc<crate::index::file_index::FileIndex>,
91    )],
92    position: tower_lsp::lsp_types::Position,
93) -> Option<Location> {
94    use crate::index::file_index::ClassKind;
95    use crate::text::word_at_position;
96    let word = word_at_position(source, position)?;
97    let bare = strip_variable_sigil(&word);
98
99    let precise_range = |line: u32, name_char: u32, name: &str| -> tower_lsp::lsp_types::Range {
100        let end_char = name_char + utf16_code_units(name);
101        tower_lsp::lsp_types::Range {
102            start: tower_lsp::lsp_types::Position {
103                line,
104                character: name_char,
105            },
106            end: tower_lsp::lsp_types::Position {
107                line,
108                character: end_char,
109            },
110        }
111    };
112
113    // First pass: abstract/interface declarations.
114    for (uri, idx) in indexes {
115        for cls in &idx.classes {
116            match cls.kind {
117                ClassKind::Interface => {
118                    // Interface itself.
119                    if cls.name.as_ref() == word {
120                        return Some(Location {
121                            uri: uri.clone(),
122                            range: precise_range(cls.start_line, cls.name_char, &cls.name),
123                        });
124                    }
125                    // Abstract method in interface.
126                    for m in &cls.methods {
127                        if m.name.as_ref() == word {
128                            return Some(Location {
129                                uri: uri.clone(),
130                                range: precise_range(m.start_line, m.name_char, &m.name),
131                            });
132                        }
133                    }
134                }
135                ClassKind::Trait => {
136                    // Trait abstract methods.
137                    for m in &cls.methods {
138                        if m.is_abstract && m.name.as_ref() == word {
139                            return Some(Location {
140                                uri: uri.clone(),
141                                range: precise_range(m.start_line, m.name_char, &m.name),
142                            });
143                        }
144                    }
145                }
146                _ if cls.is_abstract => {
147                    // Abstract methods in abstract classes.
148                    for m in &cls.methods {
149                        if m.is_abstract && m.name.as_ref() == word {
150                            return Some(Location {
151                                uri: uri.clone(),
152                                range: precise_range(m.start_line, m.name_char, &m.name),
153                            });
154                        }
155                    }
156                }
157                _ => {}
158            }
159        }
160    }
161
162    // Second pass: any declaration.
163    for (uri, idx) in indexes {
164        // Top-level functions.
165        for f in &idx.functions {
166            if f.name.as_ref() == word {
167                return Some(Location {
168                    uri: uri.clone(),
169                    range: precise_range(f.start_line, f.name_char, &f.name),
170                });
171            }
172        }
173
174        for cls in &idx.classes {
175            // Class/Interface/Trait/Enum declarations.
176            if cls.name.as_ref() == word {
177                return Some(Location {
178                    uri: uri.clone(),
179                    range: precise_range(cls.start_line, cls.name_char, &cls.name),
180                });
181            }
182
183            // Methods.
184            for m in &cls.methods {
185                if m.name.as_ref() == word {
186                    return Some(Location {
187                        uri: uri.clone(),
188                        range: precise_range(m.start_line, m.name_char, &m.name),
189                    });
190                }
191            }
192
193            // `@method` docblock methods — zero-width location at the tag line.
194            for dm in &cls.doc_methods {
195                if dm.name.as_ref() == word {
196                    return Some(Location {
197                        uri: uri.clone(),
198                        range: crate::text::zero_width_range(dm.start_line),
199                    });
200                }
201            }
202
203            // Properties.
204            for p in &cls.properties {
205                if p.name.as_ref() == bare {
206                    return Some(Location {
207                        uri: uri.clone(),
208                        range: precise_range(p.start_line, p.name_char, &p.name),
209                    });
210                }
211            }
212
213            // Class/Interface/Trait/Enum constants.
214            for c in &cls.constants {
215                if c.as_ref() == word {
216                    return Some(Location {
217                        uri: uri.clone(),
218                        range: precise_range(cls.start_line, cls.name_char, &cls.name),
219                    });
220                }
221            }
222
223            // Enum cases (stored in separate `cases` field).
224            if cls.kind == ClassKind::Enum {
225                for case_name in &cls.cases {
226                    if case_name.as_ref() == word {
227                        return Some(Location {
228                            uri: uri.clone(),
229                            range: precise_range(cls.start_line, cls.name_char, &cls.name),
230                        });
231                    }
232                }
233            }
234        }
235    }
236    None
237}