Skip to main content

normalize_languages/
graphql.rs

1//! GraphQL language support.
2
3use 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
10/// GraphQL language support.
11pub 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        // GraphQL uses """ for descriptions
154        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        // GraphQL has no imports
181        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        // Run cross_check_node_kinds to populate - many kinds already used
300        #[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}