1use std::ops::Range;
2
3use tree_sitter::{Node, Parser};
4
5use crate::lang::{Lang, parsable};
6
7#[derive(Debug, Clone, Default)]
10pub struct Classified {
11 pub comments: Vec<Range<usize>>,
13 pub imports: Vec<Range<usize>>,
15 pub logging: Vec<Range<usize>>,
17 pub error_line: Option<usize>,
20}
21
22pub fn classify(lang: Lang, src: &str) -> Option<Classified> {
25 let mut parser = Parser::new();
26 parser.set_language(&lang.grammar()).ok()?;
27 let tree = parser.parse(parsable(lang, src).as_ref(), None)?;
28
29 let mut classified = Classified::default();
31 let mut cursor = tree.walk();
32 'walk: loop {
33 let node = cursor.node();
34 let layer = if lang.is_comment(node.kind()) || (lang == Lang::Python && is_docstring(node))
35 {
36 Some(&mut classified.comments)
37 } else if is_import(lang, node) {
38 Some(&mut classified.imports)
39 } else if is_logging(lang, node, src) {
40 Some(&mut classified.logging)
41 } else {
42 None
43 };
44 if let Some(layer) = layer {
45 layer.push(node.byte_range());
46 } else if cursor.goto_first_child() {
47 continue;
48 }
49 while !cursor.goto_next_sibling() {
50 if !cursor.goto_parent() {
51 break 'walk;
52 }
53 }
54 }
55
56 classified.error_line = first_error(tree.root_node()).map(|node| node.start_position().row + 1);
57 Some(classified)
58}
59
60fn is_docstring(node: Node) -> bool {
62 if node.kind() != "expression_statement" || node.named_child_count() != 1 {
63 return false;
64 }
65 let Some(expr) = node.named_child(0) else {
66 return false;
67 };
68 if !matches!(expr.kind(), "string" | "concatenated_string") {
69 return false;
70 }
71 let Some(parent) = node.parent() else {
72 return false;
73 };
74 let is_body = match parent.kind() {
75 "module" => true,
76 "block" => parent.parent().is_some_and(|owner| {
77 matches!(owner.kind(), "function_definition" | "class_definition")
78 }),
79 _ => false,
80 };
81 if !is_body {
82 return false;
83 }
84 let mut cursor = parent.walk();
85 let first_statement = parent
86 .named_children(&mut cursor)
87 .find(|child| child.kind() != "comment");
88 first_statement.is_some_and(|first| first.id() == node.id())
89}
90
91fn is_import(lang: Lang, node: Node) -> bool {
92 match lang {
93 Lang::Swift => node.kind() == "import_declaration",
94 Lang::TypeScript | Lang::Tsx | Lang::JavaScript => {
95 node.kind() == "import_statement"
96 || (node.kind() == "export_statement"
97 && node.child_by_field_name("source").is_some())
98 }
99 Lang::Python => matches!(
100 node.kind(),
101 "import_statement" | "import_from_statement" | "future_import_statement"
102 ),
103 }
104}
105
106fn is_logging(lang: Lang, node: Node, src: &str) -> bool {
108 let call = match lang {
109 Lang::Swift => (node.kind() == "call_expression"
111 && node
112 .parent()
113 .is_some_and(|p| matches!(p.kind(), "statements" | "source_file")))
114 .then_some(node),
115 _ => (node.kind() == "expression_statement")
116 .then(|| node.named_child(0))
117 .flatten()
118 .filter(|child| matches!(child.kind(), "call_expression" | "call")),
119 };
120 let Some(call) = call else {
121 return false;
122 };
123 let callee = match lang {
124 Lang::Swift => call.named_child(0),
125 _ => call.child_by_field_name("function"),
126 };
127 callee
128 .and_then(|callee| src.get(callee.byte_range()))
129 .is_some_and(|callee| is_logging_callee(lang, callee))
130}
131
132fn is_logging_callee(lang: Lang, callee: &str) -> bool {
133 let callee: String = callee.chars().filter(|c| !c.is_whitespace()).collect();
134 let segments: Vec<&str> = callee
135 .split(['.', '?', '!'])
136 .filter(|s| !s.is_empty())
137 .collect();
138 let Some((&method, receivers)) = segments.split_last() else {
139 return false;
140 };
141 if receivers.is_empty() {
142 return match lang {
143 Lang::Swift => matches!(method, "print" | "debugPrint" | "NSLog" | "os_log" | "dump"),
144 Lang::Python => method == "print",
145 _ => false,
146 };
147 }
148 const LEVELS: [&str; 13] = [
149 "log",
150 "debug",
151 "info",
152 "notice",
153 "warn",
154 "warning",
155 "error",
156 "fault",
157 "trace",
158 "critical",
159 "exception",
160 "verbose",
161 "fatal",
162 ];
163 LEVELS.contains(&method)
164 && receivers.iter().any(|receiver| {
165 let receiver = receiver.to_ascii_lowercase();
166 receiver == "console"
167 || receiver == "logging"
168 || receiver == "log"
169 || receiver.ends_with("logger")
170 })
171}
172
173fn first_error(root: Node) -> Option<Node> {
174 if !root.has_error() {
175 return None;
176 }
177 let mut node = root;
178 'descend: loop {
179 if node.is_error() || node.is_missing() {
180 return Some(node);
181 }
182 let mut cursor = node.walk();
183 for child in node.children(&mut cursor) {
184 if child.has_error() || child.is_missing() {
185 node = child;
186 continue 'descend;
187 }
188 }
189 return Some(node);
190 }
191}