debtmap/analyzers/javascript/
mod.rs1mod complexity;
2mod dependencies;
3
4use crate::analyzers::Analyzer;
5use crate::core::{
6 ast::{Ast, JavaScriptAst, TypeScriptAst},
7 ComplexityMetrics, DebtItem, DebtType, FileMetrics, FunctionMetrics, Language, Priority,
8};
9use crate::debt::patterns::{
10 find_code_smells_with_suppression, find_todos_and_fixmes_with_suppression,
11};
12use crate::debt::smells::{analyze_function_smells, analyze_module_smells};
13use crate::debt::suppression::{parse_suppression_comments, SuppressionContext};
14use anyhow::{Context, Result};
15use std::path::{Path, PathBuf};
16use std::sync::Mutex;
17use tree_sitter::Parser;
18
19pub struct JavaScriptAnalyzer {
20 parser: Mutex<Parser>,
21 language: Language,
22 complexity_threshold: u32,
23}
24
25impl JavaScriptAnalyzer {
26 pub fn new_javascript() -> Result<Self> {
27 let mut parser = Parser::new();
28 parser
29 .set_language(&tree_sitter_javascript::LANGUAGE.into())
30 .context("Failed to set JavaScript language")?;
31 Ok(Self {
32 parser: Mutex::new(parser),
33 language: Language::JavaScript,
34 complexity_threshold: 10,
35 })
36 }
37
38 pub fn new_typescript() -> Result<Self> {
39 let mut parser = Parser::new();
40 parser
41 .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
42 .context("Failed to set TypeScript language")?;
43 Ok(Self {
44 parser: Mutex::new(parser),
45 language: Language::TypeScript,
46 complexity_threshold: 10,
47 })
48 }
49
50 fn parse_tree(&self, content: &str) -> Result<tree_sitter::Tree> {
51 self.parser
52 .lock()
53 .unwrap()
54 .parse(content, None)
55 .context("Failed to parse JavaScript/TypeScript code")
56 }
57
58 fn create_ast(&self, tree: tree_sitter::Tree, source: String, path: PathBuf) -> Ast {
59 match self.language {
60 Language::JavaScript => Ast::JavaScript(JavaScriptAst { tree, source, path }),
61 Language::TypeScript => Ast::TypeScript(TypeScriptAst { tree, source, path }),
62 _ => unreachable!("JavaScriptAnalyzer should only handle JS/TS"),
63 }
64 }
65
66 fn create_debt_items(
67 &self,
68 _tree: &tree_sitter::Tree,
69 source: &str,
70 path: &Path,
71 functions: &[FunctionMetrics],
72 ) -> Vec<DebtItem> {
73 let suppression_context = parse_suppression_comments(source, self.language, path);
74
75 report_unclosed_blocks(&suppression_context);
77
78 [
80 self.collect_todos_and_fixmes(source, path, &suppression_context),
81 self.collect_code_smells(source, path, &suppression_context),
82 self.collect_function_smells(functions, &suppression_context),
83 self.collect_module_smells(source, path, &suppression_context),
84 self.collect_complexity_issues(functions, path, &suppression_context),
85 ]
86 .into_iter()
87 .flatten()
88 .collect()
89 }
90
91 fn collect_todos_and_fixmes(
92 &self,
93 source: &str,
94 path: &Path,
95 suppression_context: &SuppressionContext,
96 ) -> Vec<DebtItem> {
97 find_todos_and_fixmes_with_suppression(source, path, Some(suppression_context))
98 }
99
100 fn collect_code_smells(
101 &self,
102 source: &str,
103 path: &Path,
104 suppression_context: &SuppressionContext,
105 ) -> Vec<DebtItem> {
106 find_code_smells_with_suppression(source, path, Some(suppression_context))
107 }
108
109 fn collect_function_smells(
110 &self,
111 functions: &[FunctionMetrics],
112 suppression_context: &SuppressionContext,
113 ) -> Vec<DebtItem> {
114 functions
115 .iter()
116 .flat_map(|func| analyze_function_smells(func, 0))
117 .map(|smell| smell.to_debt_item())
118 .filter(|item| !suppression_context.is_suppressed(item.line, &item.debt_type))
119 .collect()
120 }
121
122 fn collect_module_smells(
123 &self,
124 source: &str,
125 path: &Path,
126 suppression_context: &SuppressionContext,
127 ) -> Vec<DebtItem> {
128 let lines = source.lines().count();
129 analyze_module_smells(path, lines)
130 .into_iter()
131 .map(|smell| smell.to_debt_item())
132 .filter(|item| !suppression_context.is_suppressed(item.line, &item.debt_type))
133 .collect()
134 }
135
136 fn collect_complexity_issues(
137 &self,
138 functions: &[FunctionMetrics],
139 path: &Path,
140 suppression_context: &SuppressionContext,
141 ) -> Vec<DebtItem> {
142 functions
143 .iter()
144 .filter(|func| func.is_complex(self.complexity_threshold))
145 .map(|func| self.create_complexity_debt_item(func, path))
146 .filter(|item| !suppression_context.is_suppressed(item.line, &item.debt_type))
147 .collect()
148 }
149
150 fn create_complexity_debt_item(&self, func: &FunctionMetrics, path: &Path) -> DebtItem {
151 DebtItem {
152 id: format!("complexity-{}-{}", path.display(), func.line),
153 debt_type: DebtType::Complexity,
154 priority: if func.cyclomatic > 20 || func.cognitive > 20 {
155 Priority::High
156 } else {
157 Priority::Medium
158 },
159 file: path.to_path_buf(),
160 line: func.line,
161 message: format!(
162 "Function '{}' has high complexity (cyclomatic: {}, cognitive: {})",
163 func.name, func.cyclomatic, func.cognitive
164 ),
165 context: None,
166 }
167 }
168}
169
170impl Analyzer for JavaScriptAnalyzer {
171 fn parse(&self, content: &str, path: PathBuf) -> Result<Ast> {
172 let tree = self.parse_tree(content)?;
174
175 let ast = self.create_ast(tree, content.to_string(), path);
177 Ok(ast)
178 }
179
180 fn analyze(&self, ast: &Ast) -> FileMetrics {
181 match ast {
182 Ast::JavaScript(js_ast) => {
183 let root_node = js_ast.tree.root_node();
184 let functions =
185 complexity::extract_functions(root_node, &js_ast.source, &js_ast.path);
186 let dependencies = dependencies::extract_dependencies(root_node, &js_ast.source);
187 let debt_items =
188 self.create_debt_items(&js_ast.tree, &js_ast.source, &js_ast.path, &functions);
189
190 let (cyclomatic, cognitive) = functions.iter().fold((0, 0), |(cyc, cog), f| {
191 (cyc + f.cyclomatic, cog + f.cognitive)
192 });
193
194 FileMetrics {
195 path: js_ast.path.clone(),
196 language: Language::JavaScript,
197 complexity: ComplexityMetrics {
198 functions,
199 cyclomatic_complexity: cyclomatic,
200 cognitive_complexity: cognitive,
201 },
202 debt_items,
203 dependencies,
204 duplications: vec![],
205 }
206 }
207 Ast::TypeScript(ts_ast) => {
208 let root_node = ts_ast.tree.root_node();
209 let functions =
210 complexity::extract_functions(root_node, &ts_ast.source, &ts_ast.path);
211 let dependencies = dependencies::extract_dependencies(root_node, &ts_ast.source);
212 let debt_items =
213 self.create_debt_items(&ts_ast.tree, &ts_ast.source, &ts_ast.path, &functions);
214
215 let (cyclomatic, cognitive) = functions.iter().fold((0, 0), |(cyc, cog), f| {
216 (cyc + f.cyclomatic, cog + f.cognitive)
217 });
218
219 FileMetrics {
220 path: ts_ast.path.clone(),
221 language: Language::TypeScript,
222 complexity: ComplexityMetrics {
223 functions,
224 cyclomatic_complexity: cyclomatic,
225 cognitive_complexity: cognitive,
226 },
227 debt_items,
228 dependencies,
229 duplications: vec![],
230 }
231 }
232 _ => FileMetrics {
233 path: PathBuf::new(),
234 language: self.language,
235 complexity: ComplexityMetrics::default(),
236 debt_items: vec![],
237 dependencies: vec![],
238 duplications: vec![],
239 },
240 }
241 }
242
243 fn language(&self) -> Language {
244 self.language
245 }
246}
247
248fn report_unclosed_blocks(suppression_context: &SuppressionContext) {
249 for unclosed in &suppression_context.unclosed_blocks {
250 eprintln!(
251 "Warning: Unclosed suppression block starting at line {} in {}",
252 unclosed.start_line,
253 unclosed.file.display()
254 );
255 }
256}