1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct ObjC;
10
11impl Language for ObjC {
12 fn name(&self) -> &'static str {
13 "Objective-C"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["m", "mm"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "objc"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &[
28 "class_interface",
29 "class_implementation",
30 "protocol_declaration",
31 ]
32 }
33
34 fn function_kinds(&self) -> &'static [&'static str] {
35 &["method_declaration", "function_definition"]
36 }
37
38 fn type_kinds(&self) -> &'static [&'static str] {
39 &["struct_specifier", "enum_specifier", "type_definition"]
40 }
41
42 fn import_kinds(&self) -> &'static [&'static str] {
43 &["preproc_include"]
44 }
45
46 fn public_symbol_kinds(&self) -> &'static [&'static str] {
47 &[
48 "class_interface",
49 "protocol_declaration",
50 "method_declaration",
51 ]
52 }
53
54 fn visibility_mechanism(&self) -> VisibilityMechanism {
55 VisibilityMechanism::AllPublic
56 }
57
58 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
59 match node.kind() {
60 "class_interface" | "class_implementation" | "protocol_declaration" => {
61 if let Some(name) = self.node_name(node, content) {
62 return vec![Export {
63 name: name.to_string(),
64 kind: SymbolKind::Class,
65 line: node.start_position().row + 1,
66 }];
67 }
68 }
69 "method_declaration" => {
70 if let Some(name) = self.node_name(node, content) {
71 return vec![Export {
72 name: name.to_string(),
73 kind: SymbolKind::Function,
74 line: node.start_position().row + 1,
75 }];
76 }
77 }
78 _ => {}
79 }
80 Vec::new()
81 }
82
83 fn scope_creating_kinds(&self) -> &'static [&'static str] {
84 &[
85 "class_implementation",
86 "method_declaration",
87 "function_definition",
88 "compound_statement",
89 ]
90 }
91
92 fn control_flow_kinds(&self) -> &'static [&'static str] {
93 &[
94 "if_statement",
95 "switch_statement",
96 "while_statement",
97 "for_statement",
98 ]
99 }
100
101 fn complexity_nodes(&self) -> &'static [&'static str] {
102 &[
103 "if_statement",
104 "switch_statement",
105 "while_statement",
106 "for_statement",
107 ]
108 }
109
110 fn nesting_nodes(&self) -> &'static [&'static str] {
111 &[
112 "if_statement",
113 "switch_statement",
114 "while_statement",
115 "for_statement",
116 "compound_statement",
117 ]
118 }
119
120 fn signature_suffix(&self) -> &'static str {
121 ""
122 }
123
124 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
125 match node.kind() {
126 "method_declaration" | "function_definition" => {
127 let name = self.node_name(node, content)?;
128 let text = &content[node.byte_range()];
129 let first_line = text.lines().next().unwrap_or(text);
130
131 Some(Symbol {
132 name: name.to_string(),
133 kind: SymbolKind::Function,
134 signature: first_line.trim().to_string(),
135 docstring: None,
136 attributes: Vec::new(),
137 start_line: node.start_position().row + 1,
138 end_line: node.end_position().row + 1,
139 visibility: Visibility::Public,
140 children: Vec::new(),
141 is_interface_impl: false,
142 implements: Vec::new(),
143 })
144 }
145 _ => None,
146 }
147 }
148
149 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
150 match node.kind() {
151 "class_interface" | "class_implementation" | "protocol_declaration" => {
152 let name = self.node_name(node, content)?;
153 let text = &content[node.byte_range()];
154 let first_line = text.lines().next().unwrap_or(text);
155
156 Some(Symbol {
157 name: name.to_string(),
158 kind: SymbolKind::Class,
159 signature: first_line.trim().to_string(),
160 docstring: None,
161 attributes: Vec::new(),
162 start_line: node.start_position().row + 1,
163 end_line: node.end_position().row + 1,
164 visibility: Visibility::Public,
165 children: Vec::new(),
166 is_interface_impl: false,
167 implements: Vec::new(),
168 })
169 }
170 _ => None,
171 }
172 }
173
174 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
175 match node.kind() {
176 "struct_specifier" | "enum_specifier" | "type_definition" => {
177 let name = self.node_name(node, content)?;
178 let text = &content[node.byte_range()];
179 let first_line = text.lines().next().unwrap_or(text);
180
181 Some(Symbol {
182 name: name.to_string(),
183 kind: SymbolKind::Type,
184 signature: first_line.trim().to_string(),
185 docstring: None,
186 attributes: Vec::new(),
187 start_line: node.start_position().row + 1,
188 end_line: node.end_position().row + 1,
189 visibility: Visibility::Public,
190 children: Vec::new(),
191 is_interface_impl: false,
192 implements: Vec::new(),
193 })
194 }
195 _ => None,
196 }
197 }
198
199 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
200 None
201 }
202
203 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
204 Vec::new()
205 }
206
207 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
208 match node.kind() {
209 "preproc_include" => {
210 let text = &content[node.byte_range()];
211 vec![Import {
212 module: text.trim().to_string(),
213 names: Vec::new(),
214 alias: None,
215 is_wildcard: false,
216 is_relative: text.contains('"'),
217 line: node.start_position().row + 1,
218 }]
219 }
220 _ => Vec::new(),
221 }
222 }
223
224 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
225 if import.is_relative {
227 format!("#import \"{}\"", import.module)
228 } else {
229 format!("#import <{}>", import.module)
230 }
231 }
232
233 fn is_public(&self, _node: &Node, _content: &str) -> bool {
234 true
235 }
236 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
237 Visibility::Public
238 }
239
240 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
241 let name = symbol.name.as_str();
242 match symbol.kind {
243 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
244 crate::SymbolKind::Module => name == "tests" || name == "test",
245 _ => false,
246 }
247 }
248
249 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
250 None
251 }
252
253 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
254 node.child_by_field_name("body")
255 }
256
257 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
258 false
259 }
260
261 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
262 node.child_by_field_name("name")
263 .or_else(|| node.child_by_field_name("declarator"))
264 .map(|n| &content[n.byte_range()])
265 }
266
267 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
268 let ext = path.extension()?.to_str()?;
269 if !["m", "mm"].contains(&ext) {
270 return None;
271 }
272 let stem = path.file_stem()?.to_str()?;
273 Some(stem.to_string())
274 }
275
276 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
277 vec![
278 format!("{}.m", module),
279 format!("{}.mm", module),
280 format!("{}.h", module),
281 ]
282 }
283
284 fn lang_key(&self) -> &'static str {
285 "objc"
286 }
287
288 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
289 import_name.starts_with("<Foundation/")
290 || import_name.starts_with("<UIKit/")
291 || import_name.starts_with("<AppKit/")
292 }
293
294 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
295 None
296 }
297 fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
298 None
299 }
300 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
301 None
302 }
303 fn get_version(&self, _: &Path) -> Option<String> {
304 None
305 }
306 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
307 None
308 }
309 fn indexable_extensions(&self) -> &'static [&'static str] {
310 &["m", "mm", "h"]
311 }
312 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
313 Vec::new()
314 }
315
316 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
317 use crate::traits::{has_extension, skip_dotfiles};
318 if skip_dotfiles(name) {
319 return true;
320 }
321 !is_dir && !has_extension(name, self.indexable_extensions())
322 }
323
324 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
325 Vec::new()
326 }
327
328 fn package_module_name(&self, entry_name: &str) -> String {
329 entry_name
330 .strip_suffix(".m")
331 .or_else(|| entry_name.strip_suffix(".mm"))
332 .or_else(|| entry_name.strip_suffix(".h"))
333 .unwrap_or(entry_name)
334 .to_string()
335 }
336
337 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
338 if path.is_file() {
339 Some(path.to_path_buf())
340 } else {
341 None
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::validate_unused_kinds_audit;
350
351 #[test]
352 fn unused_node_kinds_audit() {
353 #[rustfmt::skip]
354 let documented_unused: &[&str] = &[
355 "preproc_if", "preproc_elif", "preproc_elifdef", "preproc_function_def",
357 "expression_statement", "return_statement", "break_statement", "continue_statement",
359 "goto_statement", "case_statement", "labeled_statement", "attributed_statement",
360 "try_statement", "catch_clause", "throw_statement",
362 "binary_expression", "unary_expression", "conditional_expression",
364 "call_expression", "subscript_expression", "cast_expression",
365 "comma_expression", "assignment_expression", "update_expression",
366 "compound_literal_expression", "generic_expression",
367 "message_expression", "selector_expression", "encode_expression",
369 "at_expression", "available_expression",
370 "declaration", "declaration_list", "field_declaration_list",
372 "property_declaration", "class_declaration", "atomic_declaration",
373 "protocol_forward_declaration", "qualified_protocol_interface_declaration",
374 "compatibility_alias_declaration",
375 "type_name", "type_identifier", "type_qualifier",
377 "sized_type_specifier", "array_type_specifier", "macro_type_specifier",
378 "typedefed_specifier", "union_specifier", "generic_specifier",
379 "method_definition", "method_identifier", "method_type",
381 "field_identifier", "statement_identifier",
383 "attribute_specifier", "attribute_declaration", "storage_class_specifier",
385 "visibility_specification", "property_attributes_declaration",
386 "protocol_qualifier", "alignas_qualifier", "alignof_expression",
387 "availability_attribute_specifier", "platform",
388 "ms_restrict_modifier", "ms_unaligned_ptr_modifier", "ms_based_modifier",
390 "ms_signed_ptr_modifier", "ms_pointer_modifier", "ms_call_modifier",
391 "ms_declspec_modifier", "ms_unsigned_ptr_modifier", "ms_asm_block",
392 "gnu_asm_expression", "va_arg_expression", "offsetof_expression",
394 "function_declarator", "enumerator", "enumerator_list", "else_clause",
396 "module_import", "abstract_block_pointer_declarator",
397 "extension_expression", "pointer_expression", "parenthesized_expression",
399 "sizeof_expression", "range_expression", "field_expression", "block_literal",
400 "implementation_definition", "struct_declaration", "field_declaration",
402 "parameter_declaration", "linkage_specification",
403 "do_statement", "synchronized_statement", "finally_clause",
404 "typeof_specifier", "type_descriptor", "primitive_type",
406 "preproc_else", "preproc_ifdef",
408 "method_parameter", "block_pointer_declarator", "abstract_function_declarator",
410 "bitfield_clause", "identifier", "struct_declarator", "gnu_asm_qualifier",
411 ];
412 validate_unused_kinds_audit(&ObjC, documented_unused)
413 .expect("Objective-C unused node kinds audit failed");
414 }
415}