normalize_languages/
groovy.rs1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub struct Groovy;
8
9impl Language for Groovy {
10 fn name(&self) -> &'static str {
11 "Groovy"
12 }
13 fn extensions(&self) -> &'static [&'static str] {
14 &["groovy", "gradle", "gvy", "gy", "gsh"]
15 }
16 fn grammar_name(&self) -> &'static str {
17 "groovy"
18 }
19
20 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21 Some(self)
22 }
23
24 fn signature_suffix(&self) -> &'static str {
25 " {}"
26 }
27
28 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
29 node.child_by_field_name("name")
31 .or_else(|| node.child_by_field_name("function"))
32 .map(|n| &content[n.byte_range()])
33 }
34
35 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
36 extract_groovydoc(node, content)
37 }
38
39 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
40 extract_groovy_annotations(node, content)
41 }
42
43 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
44 if node.kind() != "groovy_import" {
45 return Vec::new();
46 }
47
48 let text = &content[node.byte_range()];
49 let line = node.start_position().row + 1;
50
51 if let Some(rest) = text.strip_prefix("import ") {
53 let rest = rest.strip_prefix("static ").unwrap_or(rest);
54 let module = rest.trim().trim_end_matches(';').to_string();
55 let is_wildcard = module.ends_with(".*");
56
57 return vec![Import {
58 module: module.trim_end_matches(".*").to_string(),
59 names: Vec::new(),
60 alias: None,
61 is_wildcard,
62 is_relative: false,
63 line,
64 }];
65 }
66
67 Vec::new()
68 }
69
70 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
71 let names_to_use: Vec<&str> = names
73 .map(|n| n.to_vec())
74 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
75 if import.is_wildcard {
76 format!("import {}.*", import.module)
77 } else if names_to_use.is_empty() {
78 format!("import {}", import.module)
79 } else if names_to_use.len() == 1 {
80 format!("import {}.{}", import.module, names_to_use[0])
81 } else {
82 format!("import {}", import.module)
84 }
85 }
86
87 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
88 let text = &content[node.byte_range()];
89 if text.starts_with("private") {
90 Visibility::Private
91 } else if text.starts_with("protected") {
92 Visibility::Protected
93 } else {
94 Visibility::Public
95 }
96 }
97
98 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
99 let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
100 if has_test_attr {
101 return true;
102 }
103 match symbol.kind {
104 crate::SymbolKind::Class => {
105 symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
106 }
107 _ => false,
108 }
109 }
110
111 fn test_file_globs(&self) -> &'static [&'static str] {
112 &[
113 "**/src/test/**/*.groovy",
114 "**/*Test.groovy",
115 "**/*Spec.groovy",
116 ]
117 }
118
119 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
120 node.child_by_field_name("body")
121 }
122
123 fn analyze_container_body(
124 &self,
125 body_node: &Node,
126 content: &str,
127 inner_indent: &str,
128 ) -> Option<ContainerBody> {
129 crate::body::analyze_brace_body(body_node, content, inner_indent)
130 }
131}
132
133impl LanguageSymbols for Groovy {}
134
135fn extract_groovydoc(node: &Node, content: &str) -> Option<String> {
142 let parent = node.parent()?;
143 if parent.kind() != "groovy_doc" {
144 return None;
145 }
146
147 let mut doc_parts: Vec<String> = Vec::new();
148 let mut cursor = parent.walk();
149 for child in parent.children(&mut cursor) {
150 match child.kind() {
151 "first_line" => {
152 let text = content[child.byte_range()].trim();
153 let text = text.strip_suffix("*/").unwrap_or(text).trim();
155 if !text.is_empty() {
156 doc_parts.push(text.to_string());
157 }
158 }
159 "tag_value" => {
160 let text = content[child.byte_range()].trim();
161 if !text.is_empty() {
162 doc_parts.push(text.to_string());
163 }
164 }
165 _ => {}
166 }
167 }
168
169 if doc_parts.is_empty() {
170 None
171 } else {
172 Some(doc_parts.join(" "))
173 }
174}
175
176fn extract_groovy_annotations(node: &Node, content: &str) -> Vec<String> {
181 let mut attrs = Vec::new();
182 let mut cursor = node.walk();
183 for child in node.children(&mut cursor) {
184 if child.kind() == "annotation" {
185 attrs.push(content[child.byte_range()].to_string());
186 }
187 }
188 attrs
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::validate_unused_kinds_audit;
195
196 #[test]
197 fn unused_node_kinds_audit() {
198 #[rustfmt::skip]
199 let documented_unused: &[&str] = &[
200 "access_modifier", "array_type", "builtintype", "declaration",
201 "do_while_loop", "dotted_identifier", "for_parameters",
202 "function_call", "function_declaration", "groovy_doc_throws",
203 "identifier", "juxt_function_call", "modifier",
204 "parenthesized_expression", "qualified_name", "return", "switch_block",
205 "type_with_generics", "wildcard_import",
206 "switch_statement",
208 "if_statement",
209 "groovy_import",
210 "while_loop",
211 "try_statement",
212 "for_in_loop",
213 "for_loop",
214 "case",
215 ];
216 validate_unused_kinds_audit(&Groovy, documented_unused)
217 .expect("Groovy unused node kinds audit failed");
218 }
219}