debtmap/analyzers/javascript/
mod.rs

1mod 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
76        report_unclosed_blocks(&suppression_context);
77
78        // Collect all debt items using functional approach
79        [
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        // Parse the content directly using the already configured parser
173        let tree = self.parse_tree(content)?;
174
175        // Create the appropriate AST type based on language
176        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}