1use crate::external_packages::ResolvedPackage;
4use crate::{
5 Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism, simple_symbol,
6};
7use std::path::{Path, PathBuf};
8use tree_sitter::Node;
9
10pub struct GraphQL;
12
13impl Language for GraphQL {
14 fn name(&self) -> &'static str {
15 "GraphQL"
16 }
17 fn extensions(&self) -> &'static [&'static str] {
18 &["graphql", "gql"]
19 }
20 fn grammar_name(&self) -> &'static str {
21 "graphql"
22 }
23
24 fn has_symbols(&self) -> bool {
25 true
26 }
27
28 fn container_kinds(&self) -> &'static [&'static str] {
29 &[
30 "object_type_definition",
31 "interface_type_definition",
32 "enum_type_definition",
33 "union_type_definition",
34 "input_object_type_definition",
35 ]
36 }
37
38 fn function_kinds(&self) -> &'static [&'static str] {
39 &["field_definition", "operation_definition"]
40 }
41
42 fn type_kinds(&self) -> &'static [&'static str] {
43 &[
44 "object_type_definition",
45 "interface_type_definition",
46 "enum_type_definition",
47 "union_type_definition",
48 "input_object_type_definition",
49 "scalar_type_definition",
50 ]
51 }
52
53 fn import_kinds(&self) -> &'static [&'static str] {
54 &[]
55 }
56
57 fn public_symbol_kinds(&self) -> &'static [&'static str] {
58 &[
59 "object_type_definition",
60 "interface_type_definition",
61 "operation_definition",
62 ]
63 }
64
65 fn visibility_mechanism(&self) -> VisibilityMechanism {
66 VisibilityMechanism::NotApplicable
67 }
68
69 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
70 let name = match self.node_name(node, content) {
71 Some(n) => n.to_string(),
72 None => return Vec::new(),
73 };
74
75 let kind = match node.kind() {
76 "object_type_definition" => SymbolKind::Struct,
77 "interface_type_definition" => SymbolKind::Interface,
78 "enum_type_definition" | "union_type_definition" => SymbolKind::Enum,
79 "input_object_type_definition" => SymbolKind::Struct,
80 "scalar_type_definition" => SymbolKind::Type,
81 "operation_definition" => SymbolKind::Function,
82 "field_definition" => SymbolKind::Method,
83 _ => return Vec::new(),
84 };
85
86 vec![Export {
87 name,
88 kind,
89 line: node.start_position().row + 1,
90 }]
91 }
92
93 fn scope_creating_kinds(&self) -> &'static [&'static str] {
94 &["selection_set"]
95 }
96
97 fn control_flow_kinds(&self) -> &'static [&'static str] {
98 &[]
99 }
100 fn complexity_nodes(&self) -> &'static [&'static str] {
101 &["selection_set"]
102 }
103 fn nesting_nodes(&self) -> &'static [&'static str] {
104 &["selection_set", "object_type_definition"]
105 }
106
107 fn signature_suffix(&self) -> &'static str {
108 ""
109 }
110
111 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
112 let name = self.node_name(node, content)?;
113 Some(simple_symbol(
114 node,
115 content,
116 name,
117 SymbolKind::Method,
118 self.extract_docstring(node, content),
119 ))
120 }
121
122 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
123 let name = self.node_name(node, content)?;
124 let (kind, keyword) = match node.kind() {
125 "interface_type_definition" => (SymbolKind::Interface, "interface"),
126 "enum_type_definition" => (SymbolKind::Enum, "enum"),
127 "union_type_definition" => (SymbolKind::Enum, "union"),
128 "input_object_type_definition" => (SymbolKind::Struct, "input"),
129 "scalar_type_definition" => (SymbolKind::Type, "scalar"),
130 _ => (SymbolKind::Struct, "type"),
131 };
132
133 Some(Symbol {
134 name: name.to_string(),
135 kind,
136 signature: format!("{} {}", keyword, name),
137 docstring: self.extract_docstring(node, content),
138 attributes: Vec::new(),
139 start_line: node.start_position().row + 1,
140 end_line: node.end_position().row + 1,
141 visibility: Visibility::Public,
142 children: Vec::new(),
143 is_interface_impl: false,
144 implements: Vec::new(),
145 })
146 }
147
148 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
149 self.extract_container(node, content)
150 }
151
152 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
153 let mut prev = node.prev_sibling();
155 while let Some(sibling) = prev {
156 let text = &content[sibling.byte_range()];
157 if sibling.kind() == "description" || text.starts_with("\"\"\"") {
158 let inner = text
159 .trim_start_matches("\"\"\"")
160 .trim_end_matches("\"\"\"")
161 .trim();
162 if !inner.is_empty() {
163 return Some(inner.to_string());
164 }
165 }
166 prev = sibling.prev_sibling();
167 }
168 None
169 }
170
171 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
172 Vec::new()
173 }
174
175 fn extract_imports(&self, _node: &Node, _content: &str) -> Vec<Import> {
176 Vec::new()
177 }
178
179 fn format_import(&self, _import: &Import, _names: Option<&[&str]>) -> String {
180 String::new()
182 }
183
184 fn is_public(&self, _node: &Node, _content: &str) -> bool {
185 true
186 }
187 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
188 Visibility::Public
189 }
190
191 fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
192 false
193 }
194
195 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
196 None
197 }
198
199 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
200 node.child_by_field_name("fields_definition")
201 }
202
203 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
204 false
205 }
206
207 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
208 node.child_by_field_name("name")
209 .map(|n| &content[n.byte_range()])
210 }
211
212 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
213 let ext = path.extension()?.to_str()?;
214 if ext != "graphql" && ext != "gql" {
215 return None;
216 }
217 let stem = path.file_stem()?.to_str()?;
218 Some(stem.to_string())
219 }
220
221 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
222 vec![format!("{}.graphql", module), format!("{}.gql", module)]
223 }
224
225 fn lang_key(&self) -> &'static str {
226 "graphql"
227 }
228
229 fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
230 false
231 }
232 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
233 None
234 }
235 fn resolve_local_import(
236 &self,
237 _import: &str,
238 _current_file: &Path,
239 _project_root: &Path,
240 ) -> Option<PathBuf> {
241 None
242 }
243 fn resolve_external_import(
244 &self,
245 _import_name: &str,
246 _project_root: &Path,
247 ) -> Option<ResolvedPackage> {
248 None
249 }
250 fn get_version(&self, _project_root: &Path) -> Option<String> {
251 None
252 }
253 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
254 None
255 }
256 fn indexable_extensions(&self) -> &'static [&'static str] {
257 &["graphql", "gql"]
258 }
259 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
260 Vec::new()
261 }
262
263 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
264 use crate::traits::{has_extension, skip_dotfiles};
265 if skip_dotfiles(name) {
266 return true;
267 }
268 !is_dir && !has_extension(name, self.indexable_extensions())
269 }
270
271 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
272 Vec::new()
273 }
274
275 fn package_module_name(&self, entry_name: &str) -> String {
276 entry_name
277 .strip_suffix(".graphql")
278 .or_else(|| entry_name.strip_suffix(".gql"))
279 .unwrap_or(entry_name)
280 .to_string()
281 }
282
283 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
284 if path.is_file() {
285 Some(path.to_path_buf())
286 } else {
287 None
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::validate_unused_kinds_audit;
296
297 #[test]
298 fn unused_node_kinds_audit() {
299 #[rustfmt::skip]
301 let documented_unused: &[&str] = &[
302 "argument", "directive", "enum_value", "enum_value_definition",
303 "enum_values_definition", "executable_definition", "field",
304 "fields_definition", "fragment_definition", "fragment_spread",
305 "implements_interfaces", "inline_fragment", "input_fields_definition",
306 "input_value_definition", "named_type", "type", "type_condition",
307 "type_definition", "type_extension", "type_system_definition",
308 "type_system_extension", "union_member_types", "variable_definition",
309 "arguments_definition", "definition", "directive_definition", "list_type",
310 "non_null_type", "object_type_extension", "operation_type",
311 "root_operation_type_definition", "scalar_type_extension", "schema_definition",
312 "enum_type_extension", "input_object_type_extension", "interface_type_extension",
313 "type_system_directive_location", "union_type_extension", "variable_definitions",
314 ];
315 validate_unused_kinds_audit(&GraphQL, documented_unused)
316 .expect("GraphQL unused node kinds audit failed");
317 }
318}