1pub mod relations;
12
13pub use relations::PhpGraphBuilder;
15
16use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
17use sqry_core::plugin::{
18 LanguageMetadata, LanguagePlugin,
19 error::{ParseError, ScopeError},
20};
21use std::path::Path;
22use streaming_iterator::StreamingIterator;
23use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
24
25pub struct PhpPlugin {
49 graph_builder: PhpGraphBuilder,
50}
51
52impl PhpPlugin {
53 #[must_use]
55 pub fn new() -> Self {
56 Self {
57 graph_builder: PhpGraphBuilder::default(),
58 }
59 }
60}
61
62impl Default for PhpPlugin {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl LanguagePlugin for PhpPlugin {
69 fn metadata(&self) -> LanguageMetadata {
70 LanguageMetadata {
71 id: "php",
72 name: "PHP",
73 version: env!("CARGO_PKG_VERSION"),
74 author: "Verivus Pty Ltd",
75 description: "PHP language support for sqry - web application code search",
76 tree_sitter_version: "0.24",
77 }
78 }
79
80 fn extensions(&self) -> &'static [&'static str] {
81 &["php"]
82 }
83
84 fn language(&self) -> Language {
85 tree_sitter_php::LANGUAGE_PHP.into()
86 }
87
88 fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
89 let mut parser = Parser::new();
90 let language = self.language();
91
92 parser.set_language(&language).map_err(|e| {
93 ParseError::LanguageSetFailed(format!("Failed to set PHP language: {e}"))
94 })?;
95
96 parser
97 .parse(content, None)
98 .ok_or(ParseError::TreeSitterFailed)
99 }
100
101 fn extract_scopes(
102 &self,
103 tree: &Tree,
104 content: &[u8],
105 file_path: &Path,
106 ) -> Result<Vec<Scope>, ScopeError> {
107 Self::extract_php_scopes(tree, content, file_path)
108 }
109 fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
110 Some(&self.graph_builder)
111 }
112}
113
114impl PhpPlugin {
115 fn extract_php_scopes(
117 tree: &Tree,
118 content: &[u8],
119 file_path: &Path,
120 ) -> Result<Vec<Scope>, ScopeError> {
121 let root_node = tree.root_node();
122 let language: Language = tree_sitter_php::LANGUAGE_PHP.into();
123
124 let scope_query = r"
126(namespace_definition
127 name: (namespace_name) @namespace.name
128) @namespace.type
129
130(class_declaration
131 name: (name) @class.name
132) @class.type
133
134(trait_declaration
135 name: (name) @trait.name
136) @trait.type
137
138(interface_declaration
139 name: (name) @interface.name
140) @interface.type
141
142(function_definition
143 name: (name) @function.name
144) @function.type
145
146(method_declaration
147 name: (name) @method.name
148) @method.type
149";
150
151 let query = Query::new(&language, scope_query)
152 .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
153
154 let mut scopes = Vec::new();
155 let mut cursor = QueryCursor::new();
156 let mut query_matches = cursor.matches(&query, root_node, content);
157
158 while let Some(m) = query_matches.next() {
159 let mut scope_type = None;
160 let mut scope_name = None;
161 let mut scope_start = None;
162 let mut scope_end = None;
163
164 for capture in m.captures {
165 let capture_name = query.capture_names()[capture.index as usize];
166 let node = capture.node;
167
168 if let Some((prefix, ext)) = capture_name.rsplit_once('.') {
169 if ext.eq_ignore_ascii_case("type") {
170 scope_type = Some(prefix.to_string());
171 scope_start = Some(node.start_position());
172 scope_end = Some(node.end_position());
173 } else if ext.eq_ignore_ascii_case("name") {
174 scope_name = node
175 .utf8_text(content)
176 .ok()
177 .map(std::string::ToString::to_string);
178 }
179 }
180 }
181
182 if let (Some(stype), Some(sname), Some(start), Some(end)) =
183 (scope_type, scope_name, scope_start, scope_end)
184 {
185 let normalized_type = match stype.as_str() {
187 "namespace" => "namespace",
188 "class" | "trait" | "interface" => "class",
189 "function" | "method" => "function",
190 other => other,
191 };
192
193 scopes.push(Scope {
194 id: ScopeId::new(0), scope_type: normalized_type.to_string(),
196 name: sname,
197 file_path: file_path.to_path_buf(),
198 start_line: start.row + 1,
199 start_column: start.column,
200 end_line: end.row + 1,
201 end_column: end.column,
202 parent_id: None,
203 });
204 }
205 }
206
207 scopes.sort_by_key(|s| (s.start_line, s.start_column));
209
210 link_nested_scopes(&mut scopes);
211 Ok(scopes)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_metadata() {
221 let plugin = PhpPlugin::default();
222 let metadata = plugin.metadata();
223
224 assert_eq!(metadata.id, "php");
225 assert_eq!(metadata.name, "PHP");
226 assert_eq!(metadata.version, env!("CARGO_PKG_VERSION"));
227 assert_eq!(metadata.author, "Verivus Pty Ltd");
228 assert_eq!(metadata.tree_sitter_version, "0.24");
229 }
230
231 #[test]
232 fn test_extensions() {
233 let plugin = PhpPlugin::default();
234 let extensions = plugin.extensions();
235
236 assert_eq!(extensions.len(), 1);
237 assert!(extensions.contains(&"php"));
238 }
239
240 #[test]
241 fn test_language() {
242 let plugin = PhpPlugin::default();
243 let language = plugin.language();
244
245 assert!(language.abi_version() > 0);
247 }
248
249 #[test]
250 fn test_parse_ast_simple() {
251 let plugin = PhpPlugin::default();
252 let source = b"<?php class MyClass { }";
253
254 let tree = plugin.parse_ast(source).unwrap();
255 assert!(!tree.root_node().has_error());
256 }
257
258 #[test]
259 fn test_plugin_is_send_sync() {
260 fn assert_send_sync<T: Send + Sync>() {}
261 assert_send_sync::<PhpPlugin>();
262 }
263}