1#![warn(missing_docs)]
2
3use nargo_ir::{JsExpr, JsProgram, JsStmt};
4use nargo_types::{NargoValue, Result, Span};
5use std::{
6 collections::{HashMap, HashSet},
7 fs::File,
8 io::Write,
9 path::Path,
10};
11
12#[derive(Debug, Clone, Default)]
13pub struct ScriptMetadata {
14 pub signals: HashSet<String>,
15 pub computed: HashSet<String>,
16 pub props: HashSet<String>,
17 pub emits: HashSet<String>,
18 pub actions: HashSet<String>,
19 pub dependencies: HashMap<String, HashSet<String>>, }
21
22impl ScriptMetadata {
23 pub fn to_nargo_value(&self) -> NargoValue {
24 let mut map = HashMap::new();
25
26 let signals_arr = self.signals.iter().map(|s| NargoValue::String(s.clone())).collect();
27 map.insert("signals".to_string(), NargoValue::Array(signals_arr));
28
29 let computed_arr = self.computed.iter().map(|s| NargoValue::String(s.clone())).collect();
30 map.insert("computed".to_string(), NargoValue::Array(computed_arr));
31
32 let props_arr = self.props.iter().map(|s| NargoValue::String(s.clone())).collect();
33 map.insert("props".to_string(), NargoValue::Array(props_arr));
34
35 let emits_arr = self.emits.iter().map(|s| NargoValue::String(s.clone())).collect();
36 map.insert("emits".to_string(), NargoValue::Array(emits_arr));
37
38 let actions_arr = self.actions.iter().map(|s| NargoValue::String(s.clone())).collect();
39 map.insert("actions".to_string(), NargoValue::Array(actions_arr));
40
41 let mut deps_map = HashMap::new();
42 for (k, v) in &self.dependencies {
43 let deps_arr = v.iter().map(|s| NargoValue::String(s.clone())).collect();
44 deps_map.insert(k.clone(), NargoValue::Array(deps_arr));
45 }
46 map.insert("dependencies".to_string(), NargoValue::Object(deps_map));
47
48 NargoValue::Object(map)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
54pub enum IssueLevel {
55 Error,
57 Warning,
59 Info,
61}
62
63#[derive(Debug, Clone)]
65pub struct AnalysisIssue {
66 pub level: IssueLevel,
68 pub code: String,
70 pub message: String,
72 pub span: Option<Span>,
74}
75
76#[derive(Debug, Clone, Default)]
78pub struct AnalysisReport {
79 pub file_path: Option<String>,
81 pub issues: Vec<AnalysisIssue>,
83 pub duration_ms: u64,
85}
86
87impl AnalysisReport {
88 pub fn new(file_path: Option<String>) -> Self {
90 Self { file_path, issues: Vec::new(), duration_ms: 0 }
91 }
92
93 pub fn add_issue(&mut self, level: IssueLevel, code: String, message: String, span: Option<Span>) {
95 self.issues.push(AnalysisIssue { level, code, message, span });
96 }
97
98 pub fn sort_issues(&mut self) {
100 self.issues.sort_by(|a, b| a.level.cmp(&b.level));
101 }
102
103 pub fn generate_console_report(&self) -> String {
105 let mut report = String::new();
106
107 if let Some(file_path) = &self.file_path {
108 report.push_str(&format!(
109 "Analysis report for: {}
110",
111 file_path
112 ));
113 }
114 else {
115 report.push_str(
116 "Analysis report
117",
118 );
119 }
120
121 report.push_str(&format!(
122 "Duration: {}ms
123",
124 self.duration_ms
125 ));
126 report.push_str(&format!(
127 "Total issues: {}
128",
129 self.issues.len()
130 ));
131
132 let error_count = self.issues.iter().filter(|i| i.level == IssueLevel::Error).count();
133 let warning_count = self.issues.iter().filter(|i| i.level == IssueLevel::Warning).count();
134 let info_count = self.issues.iter().filter(|i| i.level == IssueLevel::Info).count();
135
136 report.push_str(&format!(
137 "Errors: {}, Warnings: {}, Info: {}
138
139",
140 error_count, warning_count, info_count
141 ));
142
143 for (i, issue) in self.issues.iter().enumerate() {
144 let level_str = match issue.level {
145 IssueLevel::Error => "ERROR",
146 IssueLevel::Warning => "WARNING",
147 IssueLevel::Info => "INFO",
148 };
149
150 report.push_str(&format!(
151 "{}. [{}] [{}] {}
152",
153 i + 1,
154 level_str,
155 issue.code,
156 issue.message
157 ));
158
159 if let Some(span) = &issue.span {
160 report.push_str(&format!(
161 " Location: line {}, column {}
162",
163 span.start.line, span.start.column
164 ));
165 }
166
167 report.push_str("\n");
168 }
169
170 report
171 }
172
173 pub fn generate_json_report(&self) -> Result<NargoValue> {
175 let mut issues = Vec::new();
176
177 for issue in &self.issues {
178 let mut issue_map = HashMap::new();
179 issue_map.insert(
180 "level".to_string(),
181 NargoValue::String(match issue.level {
182 IssueLevel::Error => "error".to_string(),
183 IssueLevel::Warning => "warning".to_string(),
184 IssueLevel::Info => "info".to_string(),
185 }),
186 );
187 issue_map.insert("code".to_string(), NargoValue::String(issue.code.clone()));
188 issue_map.insert("message".to_string(), NargoValue::String(issue.message.clone()));
189
190 if let Some(span) = &issue.span {
191 let mut span_map = HashMap::new();
192 span_map.insert("start_line".to_string(), NargoValue::Number(span.start.line as f64));
193 span_map.insert("start_column".to_string(), NargoValue::Number(span.start.column as f64));
194 span_map.insert("end_line".to_string(), NargoValue::Number(span.end.line as f64));
195 span_map.insert("end_column".to_string(), NargoValue::Number(span.end.column as f64));
196 issue_map.insert("span".to_string(), NargoValue::Object(span_map));
197 }
198
199 issues.push(NargoValue::Object(issue_map));
200 }
201
202 let mut report_map = HashMap::new();
203 if let Some(file_path) = &self.file_path {
204 report_map.insert("file_path".to_string(), NargoValue::String(file_path.clone()));
205 }
206 report_map.insert("issues".to_string(), NargoValue::Array(issues));
207 report_map.insert("duration_ms".to_string(), NargoValue::Number(self.duration_ms as f64));
208 report_map.insert("total_issues".to_string(), NargoValue::Number(self.issues.len() as f64));
209
210 Ok(NargoValue::Object(report_map))
211 }
212
213 pub fn save_to_file(&self, output_path: &str) -> Result<()> {
215 let json_report = self.generate_json_report()?;
216 let report_str = serde_json::to_string_pretty(&json_report).map_err(|e| nargo_types::Error::external_error("serde_json".to_string(), e.to_string(), Span::default()))?;
217
218 let path = Path::new(output_path);
219 let mut file = File::create(path)?;
220 write!(file, "{}", report_str)?;
221
222 Ok(())
223 }
224}
225
226pub trait Rule {
228 fn code(&self) -> String;
230 fn description(&self) -> String;
232 fn check(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport);
234}
235
236#[derive(Default)]
238pub struct RuleEngine {
239 rules: Vec<Box<dyn Rule>>,
240}
241
242impl RuleEngine {
243 pub fn new() -> Self {
245 Self { rules: Vec::new() }
246 }
247
248 pub fn add_rule(&mut self, rule: Box<dyn Rule>) {
250 self.rules.push(rule);
251 }
252
253 pub fn run(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport) {
255 for rule in &self.rules {
256 rule.check(program, meta, report);
257 }
258 }
259}
260
261pub struct UnusedVariableRule;
263
264impl Rule for UnusedVariableRule {
265 fn code(&self) -> String {
266 "unused-variable".to_string()
267 }
268
269 fn description(&self) -> String {
270 "检查未使用的变量".to_string()
271 }
272
273 fn check(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport) {
274 let mut declared_vars = HashSet::new();
275 let mut used_vars = HashSet::new();
276
277 for stmt in &program.body {
279 match stmt {
280 JsStmt::VariableDecl { id, .. } => {
281 if id.starts_with('[') && id.ends_with(']') {
282 let content = &id[1..id.len() - 1];
283 for part in content.split(',') {
284 let trimmed = part.trim();
285 if !trimmed.is_empty() {
286 declared_vars.insert(trimmed.to_string());
287 }
288 }
289 }
290 else {
291 declared_vars.insert(id.to_string());
292 }
293 }
294 _ => {}
295 }
296 }
297
298 fn collect_used_vars(expr: &JsExpr, used_vars: &mut HashSet<String>) {
300 match expr {
301 JsExpr::Identifier(name, _, _) => {
302 used_vars.insert(name.clone());
303 }
304 JsExpr::Binary { left, right, .. } => {
305 collect_used_vars(left, used_vars);
306 collect_used_vars(right, used_vars);
307 }
308 JsExpr::Unary { argument, .. } => {
309 collect_used_vars(argument, used_vars);
310 }
311 JsExpr::Call { callee, args, .. } => {
312 collect_used_vars(callee, used_vars);
313 for arg in args {
314 collect_used_vars(arg, used_vars);
315 }
316 }
317 JsExpr::Member { object, property, computed, .. } => {
318 collect_used_vars(object, used_vars);
319 if *computed {
320 collect_used_vars(property, used_vars);
321 }
322 }
323 JsExpr::Array(elements, _, _) => {
324 for el in elements {
325 collect_used_vars(el, used_vars);
326 }
327 }
328 JsExpr::Object(properties, _, _) => {
329 for value in properties.values() {
330 collect_used_vars(value, used_vars);
331 }
332 }
333 JsExpr::ArrowFunction { body, .. } => {
334 collect_used_vars(body, used_vars);
335 }
336 JsExpr::Conditional { test, consequent, alternate, .. } => {
337 collect_used_vars(test, used_vars);
338 collect_used_vars(consequent, used_vars);
339 collect_used_vars(alternate, used_vars);
340 }
341 JsExpr::TemplateLiteral { expressions, .. } => {
342 for e in expressions {
343 collect_used_vars(e, used_vars);
344 }
345 }
346 _ => {}
347 }
348 }
349
350 fn collect_used_vars_in_stmt(stmt: &JsStmt, used_vars: &mut HashSet<String>) {
351 match stmt {
352 JsStmt::Expr(expr, _, _) => collect_used_vars(expr, used_vars),
353 JsStmt::VariableDecl { init, .. } => {
354 if let Some(e) = init {
355 collect_used_vars(e, used_vars);
356 }
357 }
358 JsStmt::Return(expr, _, _) => {
359 if let Some(e) = expr {
360 collect_used_vars(e, used_vars);
361 }
362 }
363 JsStmt::If { test, consequent, alternate, .. } => {
364 collect_used_vars(test, used_vars);
365 collect_used_vars_in_stmt(consequent, used_vars);
366 if let Some(alt) = alternate {
367 collect_used_vars_in_stmt(alt, used_vars);
368 }
369 }
370 JsStmt::While { test, body, .. } => {
371 collect_used_vars(test, used_vars);
372 collect_used_vars_in_stmt(body, used_vars);
373 }
374 JsStmt::For { init, test, update, body, .. } => {
375 if let Some(i) = init {
376 collect_used_vars_in_stmt(i, used_vars);
377 }
378 if let Some(t) = test {
379 collect_used_vars(t, used_vars);
380 }
381 if let Some(u) = update {
382 collect_used_vars(u, used_vars);
383 }
384 collect_used_vars_in_stmt(body, used_vars);
385 }
386 JsStmt::Block(stmts, _, _) => {
387 for s in stmts {
388 collect_used_vars_in_stmt(s, used_vars);
389 }
390 }
391 _ => {}
392 }
393 }
394
395 for stmt in &program.body {
396 collect_used_vars_in_stmt(stmt, &mut used_vars);
397 }
398
399 for var in &declared_vars {
401 if !used_vars.contains(var) && !meta.signals.contains(var) && !meta.computed.contains(var) && !meta.actions.contains(var) {
402 report.add_issue(IssueLevel::Warning, self.code(), format!("Unused variable: {}", var), None);
403 }
404 }
405 }
406}
407
408pub struct UndefinedVariableRule;
410
411impl Rule for UndefinedVariableRule {
412 fn code(&self) -> String {
413 "undefined-variable".to_string()
414 }
415
416 fn description(&self) -> String {
417 "检查使用未定义的变量".to_string()
418 }
419
420 fn check(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport) {
421 let mut declared_vars = HashSet::new();
422
423 for stmt in &program.body {
425 match stmt {
426 JsStmt::VariableDecl { id, .. } => {
427 if id.starts_with('[') && id.ends_with(']') {
428 let content = &id[1..id.len() - 1];
429 for part in content.split(',') {
430 let trimmed = part.trim();
431 if !trimmed.is_empty() {
432 declared_vars.insert(trimmed.to_string());
433 }
434 }
435 }
436 else {
437 declared_vars.insert(id.to_string());
438 }
439 }
440 JsStmt::FunctionDecl { id, .. } => {
441 declared_vars.insert(id.clone());
442 }
443 _ => {}
444 }
445 }
446
447 fn check_undefined_vars(expr: &JsExpr, declared_vars: &HashSet<String>, meta: &ScriptMetadata, report: &mut AnalysisReport) {
449 match expr {
450 JsExpr::Identifier(name, span, _) => {
451 if !declared_vars.contains(name) && !meta.signals.contains(name) && !meta.computed.contains(name) && !meta.props.contains(name) && name != "props" && name != "emit" && name != "emits" && !name.starts_with('$') {
452 report.add_issue(IssueLevel::Error, "undefined-variable".to_string(), format!("Undefined variable: {}", name), Some(*span));
453 }
454 }
455 JsExpr::Binary { left, right, .. } => {
456 check_undefined_vars(left, declared_vars, meta, report);
457 check_undefined_vars(right, declared_vars, meta, report);
458 }
459 JsExpr::Unary { argument, .. } => {
460 check_undefined_vars(argument, declared_vars, meta, report);
461 }
462 JsExpr::Call { callee, args, .. } => {
463 check_undefined_vars(callee, declared_vars, meta, report);
464 for arg in args {
465 check_undefined_vars(arg, declared_vars, meta, report);
466 }
467 }
468 JsExpr::Member { object, property, computed, .. } => {
469 check_undefined_vars(object, declared_vars, meta, report);
470 if *computed {
471 check_undefined_vars(property, declared_vars, meta, report);
472 }
473 }
474 JsExpr::Array(elements, _, _) => {
475 for el in elements {
476 check_undefined_vars(el, declared_vars, meta, report);
477 }
478 }
479 JsExpr::Object(properties, _, _) => {
480 for value in properties.values() {
481 check_undefined_vars(value, declared_vars, meta, report);
482 }
483 }
484 JsExpr::ArrowFunction { body, .. } => {
485 check_undefined_vars(body, declared_vars, meta, report);
486 }
487 JsExpr::Conditional { test, consequent, alternate, .. } => {
488 check_undefined_vars(test, declared_vars, meta, report);
489 check_undefined_vars(consequent, declared_vars, meta, report);
490 check_undefined_vars(alternate, declared_vars, meta, report);
491 }
492 JsExpr::TemplateLiteral { expressions, .. } => {
493 for e in expressions {
494 check_undefined_vars(e, declared_vars, meta, report);
495 }
496 }
497 _ => {}
498 }
499 }
500
501 fn check_undefined_vars_in_stmt(stmt: &JsStmt, declared_vars: &HashSet<String>, meta: &ScriptMetadata, report: &mut AnalysisReport) {
502 match stmt {
503 JsStmt::Expr(expr, _, _) => check_undefined_vars(expr, declared_vars, meta, report),
504 JsStmt::VariableDecl { init, .. } => {
505 if let Some(e) = init {
506 check_undefined_vars(e, declared_vars, meta, report);
507 }
508 }
509 JsStmt::Return(expr, _, _) => {
510 if let Some(e) = expr {
511 check_undefined_vars(e, declared_vars, meta, report);
512 }
513 }
514 JsStmt::If { test, consequent, alternate, .. } => {
515 check_undefined_vars(test, declared_vars, meta, report);
516 check_undefined_vars_in_stmt(consequent, declared_vars, meta, report);
517 if let Some(alt) = alternate {
518 check_undefined_vars_in_stmt(alt, declared_vars, meta, report);
519 }
520 }
521 JsStmt::While { test, body, .. } => {
522 check_undefined_vars(test, declared_vars, meta, report);
523 check_undefined_vars_in_stmt(body, declared_vars, meta, report);
524 }
525 JsStmt::For { init, test, update, body, .. } => {
526 if let Some(i) = init {
527 check_undefined_vars_in_stmt(i, declared_vars, meta, report);
528 }
529 if let Some(t) = test {
530 check_undefined_vars(t, declared_vars, meta, report);
531 }
532 if let Some(u) = update {
533 check_undefined_vars(u, declared_vars, meta, report);
534 }
535 check_undefined_vars_in_stmt(body, declared_vars, meta, report);
536 }
537 JsStmt::Block(stmts, _, _) => {
538 for s in stmts {
539 check_undefined_vars_in_stmt(s, declared_vars, meta, report);
540 }
541 }
542 _ => {}
543 }
544 }
545
546 for stmt in &program.body {
547 check_undefined_vars_in_stmt(stmt, &declared_vars, meta, report);
548 }
549 }
550}
551
552pub struct UnsafeOperationRule;
554
555impl Rule for UnsafeOperationRule {
556 fn code(&self) -> String {
557 "unsafe-operation".to_string()
558 }
559
560 fn description(&self) -> String {
561 "检查不安全的操作".to_string()
562 }
563
564 fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
565 fn check_unsafe_operations(expr: &JsExpr, report: &mut AnalysisReport) {
567 match expr {
568 JsExpr::Binary { left, right, op, span, .. } => {
569 if *op == "/" {
571 if let JsExpr::Literal(NargoValue::Number(0.0), _, _) = &**right {
572 report.add_issue(IssueLevel::Error, "unsafe-operation".to_string(), "Division by zero".to_string(), Some(*span));
573 }
574 }
575 if *op == "==" || *op == "!=" {
577 report.add_issue(IssueLevel::Warning, "unsafe-operation".to_string(), "Use of loose equality operator (==/!=) may cause type coercion issues, consider using strict equality (===/!==)".to_string(), Some(*span));
578 }
579 check_unsafe_operations(left, report);
580 check_unsafe_operations(right, report);
581 }
582 JsExpr::Call { callee, args, span, .. } => {
583 if let JsExpr::Identifier(name, _, _) = &**callee {
585 if name == "eval" {
586 report.add_issue(IssueLevel::Warning, "unsafe-operation".to_string(), "Use of eval is potentially unsafe".to_string(), Some(*span));
587 }
588 else if name == "Function" {
590 report.add_issue(IssueLevel::Warning, "unsafe-operation".to_string(), "Use of Function constructor is potentially unsafe".to_string(), Some(*span));
591 }
592 }
593 check_unsafe_operations(callee, report);
594 for arg in args {
595 check_unsafe_operations(arg, report);
596 }
597 }
598 JsExpr::Member { object, property, computed, .. } => {
599 check_unsafe_operations(object, report);
600 if *computed {
601 check_unsafe_operations(property, report);
602 }
603 }
604 JsExpr::Array(elements, _, _) => {
605 for el in elements {
606 check_unsafe_operations(el, report);
607 }
608 }
609 JsExpr::Object(properties, _, _) => {
610 for value in properties.values() {
611 check_unsafe_operations(value, report);
612 }
613 }
614 JsExpr::ArrowFunction { body, .. } => {
615 check_unsafe_operations(body, report);
616 }
617 JsExpr::Conditional { test, consequent, alternate, .. } => {
618 check_unsafe_operations(test, report);
619 check_unsafe_operations(consequent, report);
620 check_unsafe_operations(alternate, report);
621 }
622 JsExpr::TemplateLiteral { expressions, .. } => {
623 for e in expressions {
624 check_unsafe_operations(e, report);
625 }
626 }
627 _ => {}
628 }
629 }
630
631 fn check_unsafe_operations_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
632 match stmt {
633 JsStmt::Expr(expr, _, _) => check_unsafe_operations(expr, report),
634 JsStmt::VariableDecl { init, .. } => {
635 if let Some(e) = init {
636 check_unsafe_operations(e, report);
637 }
638 }
639 JsStmt::Return(expr, _, _) => {
640 if let Some(e) = expr {
641 check_unsafe_operations(e, report);
642 }
643 }
644 JsStmt::If { test, consequent, alternate, .. } => {
645 check_unsafe_operations(test, report);
646 check_unsafe_operations_in_stmt(consequent, report);
647 if let Some(alt) = alternate {
648 check_unsafe_operations_in_stmt(alt, report);
649 }
650 }
651 JsStmt::While { test, body, .. } => {
652 check_unsafe_operations(test, report);
653 check_unsafe_operations_in_stmt(body, report);
654 }
655 JsStmt::For { init, test, update, body, .. } => {
656 if let Some(i) = init {
657 check_unsafe_operations_in_stmt(i, report);
658 }
659 if let Some(t) = test {
660 check_unsafe_operations(t, report);
661 }
662 if let Some(u) = update {
663 check_unsafe_operations(u, report);
664 }
665 check_unsafe_operations_in_stmt(body, report);
666 }
667 JsStmt::Block(stmts, _, _) => {
668 for s in stmts {
669 check_unsafe_operations_in_stmt(s, report);
670 }
671 }
672 _ => {}
673 }
674 }
675
676 for stmt in &program.body {
677 check_unsafe_operations_in_stmt(stmt, report);
678 }
679 }
680}
681
682pub struct UnusedImportRule;
684
685impl Rule for UnusedImportRule {
686 fn code(&self) -> String {
687 "unused-import".to_string()
688 }
689
690 fn description(&self) -> String {
691 "检查未使用的导入".to_string()
692 }
693
694 fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
695 let mut imports = HashSet::new();
696 let mut used_imports = HashSet::new();
697
698 for stmt in &program.body {
700 match stmt {
701 JsStmt::Import { specifiers, source: _, span: _, trivia: _ } => {
702 for specifier in specifiers {
703 imports.insert(specifier.clone());
704 }
705 }
706 _ => {}
707 }
708 }
709
710 fn collect_used_identifiers(expr: &JsExpr, used: &mut HashSet<String>) {
712 match expr {
713 JsExpr::Identifier(name, _, _) => {
714 used.insert(name.clone());
715 }
716 JsExpr::Binary { left, right, .. } => {
717 collect_used_identifiers(left, used);
718 collect_used_identifiers(right, used);
719 }
720 JsExpr::Unary { argument, .. } => {
721 collect_used_identifiers(argument, used);
722 }
723 JsExpr::Call { callee, args, .. } => {
724 collect_used_identifiers(callee, used);
725 for arg in args {
726 collect_used_identifiers(arg, used);
727 }
728 }
729 JsExpr::Member { object, property, computed, .. } => {
730 collect_used_identifiers(object, used);
731 if *computed {
732 collect_used_identifiers(property, used);
733 }
734 }
735 JsExpr::Array(elements, _, _) => {
736 for el in elements {
737 collect_used_identifiers(el, used);
738 }
739 }
740 JsExpr::Object(properties, _, _) => {
741 for value in properties.values() {
742 collect_used_identifiers(value, used);
743 }
744 }
745 JsExpr::ArrowFunction { body, .. } => {
746 collect_used_identifiers(body, used);
747 }
748 JsExpr::Conditional { test, consequent, alternate, .. } => {
749 collect_used_identifiers(test, used);
750 collect_used_identifiers(consequent, used);
751 collect_used_identifiers(alternate, used);
752 }
753 JsExpr::TemplateLiteral { expressions, .. } => {
754 for e in expressions {
755 collect_used_identifiers(e, used);
756 }
757 }
758 _ => {}
759 }
760 }
761
762 fn collect_used_identifiers_in_stmt(stmt: &JsStmt, used: &mut HashSet<String>) {
763 match stmt {
764 JsStmt::Expr(expr, _, _) => collect_used_identifiers(expr, used),
765 JsStmt::VariableDecl { init, .. } => {
766 if let Some(e) = init {
767 collect_used_identifiers(e, used);
768 }
769 }
770 JsStmt::Return(expr, _, _) => {
771 if let Some(e) = expr {
772 collect_used_identifiers(e, used);
773 }
774 }
775 JsStmt::If { test, consequent, alternate, .. } => {
776 collect_used_identifiers(test, used);
777 collect_used_identifiers_in_stmt(consequent, used);
778 if let Some(alt) = alternate {
779 collect_used_identifiers_in_stmt(alt, used);
780 }
781 }
782 JsStmt::While { test, body, .. } => {
783 collect_used_identifiers(test, used);
784 collect_used_identifiers_in_stmt(body, used);
785 }
786 JsStmt::For { init, test, update, body, .. } => {
787 if let Some(i) = init {
788 collect_used_identifiers_in_stmt(i, used);
789 }
790 if let Some(t) = test {
791 collect_used_identifiers(t, used);
792 }
793 if let Some(u) = update {
794 collect_used_identifiers(u, used);
795 }
796 collect_used_identifiers_in_stmt(body, used);
797 }
798 JsStmt::Block(stmts, _, _) => {
799 for s in stmts {
800 collect_used_identifiers_in_stmt(s, used);
801 }
802 }
803 _ => {}
804 }
805 }
806
807 for stmt in &program.body {
808 collect_used_identifiers_in_stmt(stmt, &mut used_imports);
809 }
810
811 for import in &imports {
813 if !used_imports.contains(import) {
814 report.add_issue(IssueLevel::Warning, self.code(), format!("Unused import: {}", import), None);
815 }
816 }
817 }
818}
819
820pub struct MemoryLeakRule;
822
823impl Rule for MemoryLeakRule {
824 fn code(&self) -> String {
825 "memory-leak".to_string()
826 }
827
828 fn description(&self) -> String {
829 "检查可能的内存泄漏".to_string()
830 }
831
832 fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
833 let mut set_intervals = HashSet::new();
834 let mut set_timeouts = HashSet::new();
835 let mut event_listeners = HashSet::new();
836
837 fn check_memory_leaks(expr: &JsExpr, set_intervals: &mut HashSet<String>, set_timeouts: &mut HashSet<String>, event_listeners: &mut HashSet<String>, report: &mut AnalysisReport) {
839 match expr {
840 JsExpr::Call { callee, args, span, .. } => {
841 if let JsExpr::Identifier(name, _, _) = &**callee {
843 if name == "setInterval" {
844 if let Some(id_expr) = args.get(2) {
845 if let JsExpr::Identifier(id, _, _) = id_expr {
846 set_intervals.insert(id.clone());
847 }
848 }
849 else {
850 report.add_issue(IssueLevel::Warning, "memory-leak".to_string(), "setInterval without an ID may cause memory leaks".to_string(), Some(*span));
851 }
852 }
853 else if name == "setTimeout" {
855 if let Some(id_expr) = args.get(2) {
856 if let JsExpr::Identifier(id, _, _) = id_expr {
857 set_timeouts.insert(id.clone());
858 }
859 }
860 }
861 }
862 if let JsExpr::Member { object, property, computed: false, .. } = &**callee {
864 if let JsExpr::Identifier(prop_name, _, _) = &**property {
865 if prop_name == "addEventListener" {
866 event_listeners.insert(format!("{:?}.addEventListener", object));
867 }
868 }
869 }
870 check_memory_leaks(callee, set_intervals, set_timeouts, event_listeners, report);
871 for arg in args {
872 check_memory_leaks(arg, set_intervals, set_timeouts, event_listeners, report);
873 }
874 }
875 JsExpr::Member { object, property, computed, .. } => {
876 check_memory_leaks(object, set_intervals, set_timeouts, event_listeners, report);
877 if *computed {
878 check_memory_leaks(property, set_intervals, set_timeouts, event_listeners, report);
879 }
880 }
881 JsExpr::Binary { left, right, .. } => {
882 check_memory_leaks(left, set_intervals, set_timeouts, event_listeners, report);
883 check_memory_leaks(right, set_intervals, set_timeouts, event_listeners, report);
884 }
885 JsExpr::Unary { argument, .. } => {
886 check_memory_leaks(argument, set_intervals, set_timeouts, event_listeners, report);
887 }
888 JsExpr::Array(elements, _, _) => {
889 for el in elements {
890 check_memory_leaks(el, set_intervals, set_timeouts, event_listeners, report);
891 }
892 }
893 JsExpr::Object(properties, _, _) => {
894 for value in properties.values() {
895 check_memory_leaks(value, set_intervals, set_timeouts, event_listeners, report);
896 }
897 }
898 JsExpr::ArrowFunction { body, .. } => {
899 check_memory_leaks(body, set_intervals, set_timeouts, event_listeners, report);
900 }
901 JsExpr::Conditional { test, consequent, alternate, .. } => {
902 check_memory_leaks(test, set_intervals, set_timeouts, event_listeners, report);
903 check_memory_leaks(consequent, set_intervals, set_timeouts, event_listeners, report);
904 check_memory_leaks(alternate, set_intervals, set_timeouts, event_listeners, report);
905 }
906 JsExpr::TemplateLiteral { expressions, .. } => {
907 for e in expressions {
908 check_memory_leaks(e, set_intervals, set_timeouts, event_listeners, report);
909 }
910 }
911 _ => {}
912 }
913 }
914
915 fn check_memory_leaks_in_stmt(stmt: &JsStmt, set_intervals: &mut HashSet<String>, set_timeouts: &mut HashSet<String>, event_listeners: &mut HashSet<String>, report: &mut AnalysisReport) {
916 match stmt {
917 JsStmt::Expr(expr, _, _) => check_memory_leaks(expr, set_intervals, set_timeouts, event_listeners, report),
918 JsStmt::VariableDecl { init, .. } => {
919 if let Some(e) = init {
920 check_memory_leaks(e, set_intervals, set_timeouts, event_listeners, report);
921 }
922 }
923 JsStmt::Return(expr, _, _) => {
924 if let Some(e) = expr {
925 check_memory_leaks(e, set_intervals, set_timeouts, event_listeners, report);
926 }
927 }
928 JsStmt::If { test, consequent, alternate, .. } => {
929 check_memory_leaks(test, set_intervals, set_timeouts, event_listeners, report);
930 check_memory_leaks_in_stmt(consequent, set_intervals, set_timeouts, event_listeners, report);
931 if let Some(alt) = alternate {
932 check_memory_leaks_in_stmt(alt, set_intervals, set_timeouts, event_listeners, report);
933 }
934 }
935 JsStmt::While { test, body, .. } => {
936 check_memory_leaks(test, set_intervals, set_timeouts, event_listeners, report);
937 check_memory_leaks_in_stmt(body, set_intervals, set_timeouts, event_listeners, report);
938 }
939 JsStmt::For { init, test, update, body, .. } => {
940 if let Some(i) = init {
941 check_memory_leaks_in_stmt(i, set_intervals, set_timeouts, event_listeners, report);
942 }
943 if let Some(t) = test {
944 check_memory_leaks(t, set_intervals, set_timeouts, event_listeners, report);
945 }
946 if let Some(u) = update {
947 check_memory_leaks(u, set_intervals, set_timeouts, event_listeners, report);
948 }
949 check_memory_leaks_in_stmt(body, set_intervals, set_timeouts, event_listeners, report);
950 }
951 JsStmt::Block(stmts, _, _) => {
952 for s in stmts {
953 check_memory_leaks_in_stmt(s, set_intervals, set_timeouts, event_listeners, report);
954 }
955 }
956 _ => {}
957 }
958 }
959
960 for stmt in &program.body {
961 check_memory_leaks_in_stmt(stmt, &mut set_intervals, &mut set_timeouts, &mut event_listeners, report);
962 }
963
964 let mut clear_intervals = HashSet::new();
966 let mut clear_timeouts = HashSet::new();
967 let mut remove_event_listeners = HashSet::new();
968
969 fn check_clear_functions(expr: &JsExpr, clear_intervals: &mut HashSet<String>, clear_timeouts: &mut HashSet<String>, remove_event_listeners: &mut HashSet<String>) {
970 match expr {
971 JsExpr::Call { callee, args, .. } => {
972 if let JsExpr::Identifier(name, _, _) = &**callee {
973 if name == "clearInterval" {
974 if let Some(id_expr) = args.get(0) {
975 if let JsExpr::Identifier(id, _, _) = id_expr {
976 clear_intervals.insert(id.clone());
977 }
978 }
979 }
980 else if name == "clearTimeout" {
981 if let Some(id_expr) = args.get(0) {
982 if let JsExpr::Identifier(id, _, _) = id_expr {
983 clear_timeouts.insert(id.clone());
984 }
985 }
986 }
987 }
988 if let JsExpr::Member { property, computed: false, .. } = &**callee {
989 if let JsExpr::Identifier(prop_name, _, _) = &**property {
990 if prop_name == "removeEventListener" {
991 remove_event_listeners.insert("removeEventListener".to_string());
992 }
993 }
994 }
995 check_clear_functions(callee, clear_intervals, clear_timeouts, remove_event_listeners);
996 for arg in args {
997 check_clear_functions(arg, clear_intervals, clear_timeouts, remove_event_listeners);
998 }
999 }
1000 JsExpr::Member { object, property, computed, .. } => {
1001 check_clear_functions(object, clear_intervals, clear_timeouts, remove_event_listeners);
1002 if *computed {
1003 check_clear_functions(property, clear_intervals, clear_timeouts, remove_event_listeners);
1004 }
1005 }
1006 JsExpr::Binary { left, right, .. } => {
1007 check_clear_functions(left, clear_intervals, clear_timeouts, remove_event_listeners);
1008 check_clear_functions(right, clear_intervals, clear_timeouts, remove_event_listeners);
1009 }
1010 JsExpr::Unary { argument, .. } => {
1011 check_clear_functions(argument, clear_intervals, clear_timeouts, remove_event_listeners);
1012 }
1013 JsExpr::Array(elements, _, _) => {
1014 for el in elements {
1015 check_clear_functions(el, clear_intervals, clear_timeouts, remove_event_listeners);
1016 }
1017 }
1018 JsExpr::Object(properties, _, _) => {
1019 for value in properties.values() {
1020 check_clear_functions(value, clear_intervals, clear_timeouts, remove_event_listeners);
1021 }
1022 }
1023 JsExpr::ArrowFunction { body, .. } => {
1024 check_clear_functions(body, clear_intervals, clear_timeouts, remove_event_listeners);
1025 }
1026 JsExpr::Conditional { test, consequent, alternate, .. } => {
1027 check_clear_functions(test, clear_intervals, clear_timeouts, remove_event_listeners);
1028 check_clear_functions(consequent, clear_intervals, clear_timeouts, remove_event_listeners);
1029 check_clear_functions(alternate, clear_intervals, clear_timeouts, remove_event_listeners);
1030 }
1031 JsExpr::TemplateLiteral { expressions, .. } => {
1032 for e in expressions {
1033 check_clear_functions(e, clear_intervals, clear_timeouts, remove_event_listeners);
1034 }
1035 }
1036 _ => {}
1037 }
1038 }
1039
1040 fn check_clear_functions_in_stmt(stmt: &JsStmt, clear_intervals: &mut HashSet<String>, clear_timeouts: &mut HashSet<String>, remove_event_listeners: &mut HashSet<String>) {
1041 match stmt {
1042 JsStmt::Expr(expr, _, _) => check_clear_functions(expr, clear_intervals, clear_timeouts, remove_event_listeners),
1043 JsStmt::VariableDecl { init, .. } => {
1044 if let Some(e) = init {
1045 check_clear_functions(e, clear_intervals, clear_timeouts, remove_event_listeners);
1046 }
1047 }
1048 JsStmt::Return(expr, _, _) => {
1049 if let Some(e) = expr {
1050 check_clear_functions(e, clear_intervals, clear_timeouts, remove_event_listeners);
1051 }
1052 }
1053 JsStmt::If { test, consequent, alternate, .. } => {
1054 check_clear_functions(test, clear_intervals, clear_timeouts, remove_event_listeners);
1055 check_clear_functions_in_stmt(consequent, clear_intervals, clear_timeouts, remove_event_listeners);
1056 if let Some(alt) = alternate {
1057 check_clear_functions_in_stmt(alt, clear_intervals, clear_timeouts, remove_event_listeners);
1058 }
1059 }
1060 JsStmt::While { test, body, .. } => {
1061 check_clear_functions(test, clear_intervals, clear_timeouts, remove_event_listeners);
1062 check_clear_functions_in_stmt(body, clear_intervals, clear_timeouts, remove_event_listeners);
1063 }
1064 JsStmt::For { init, test, update, body, .. } => {
1065 if let Some(i) = init {
1066 check_clear_functions_in_stmt(i, clear_intervals, clear_timeouts, remove_event_listeners);
1067 }
1068 if let Some(t) = test {
1069 check_clear_functions(t, clear_intervals, clear_timeouts, remove_event_listeners);
1070 }
1071 if let Some(u) = update {
1072 check_clear_functions(u, clear_intervals, clear_timeouts, remove_event_listeners);
1073 }
1074 check_clear_functions_in_stmt(body, clear_intervals, clear_timeouts, remove_event_listeners);
1075 }
1076 JsStmt::Block(stmts, _, _) => {
1077 for s in stmts {
1078 check_clear_functions_in_stmt(s, clear_intervals, clear_timeouts, remove_event_listeners);
1079 }
1080 }
1081 _ => {}
1082 }
1083 }
1084
1085 for stmt in &program.body {
1086 check_clear_functions_in_stmt(stmt, &mut clear_intervals, &mut clear_timeouts, &mut remove_event_listeners);
1087 }
1088
1089 for interval_id in &set_intervals {
1091 if !clear_intervals.contains(interval_id) {
1092 report.add_issue(IssueLevel::Warning, self.code(), format!("setInterval with ID '{}' may not be cleared, potential memory leak", interval_id), None);
1093 }
1094 }
1095
1096 if !event_listeners.is_empty() && remove_event_listeners.is_empty() {
1098 report.add_issue(IssueLevel::Warning, self.code(), "Event listeners added but not removed, potential memory leak".to_string(), None);
1099 }
1100 }
1101}
1102
1103pub struct PerformanceBottleneckRule;
1105
1106impl Rule for PerformanceBottleneckRule {
1107 fn code(&self) -> String {
1108 "performance-bottleneck".to_string()
1109 }
1110
1111 fn description(&self) -> String {
1112 "检查可能的性能瓶颈".to_string()
1113 }
1114
1115 fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
1116 fn check_nested_loops(stmt: &JsStmt, loop_depth: u32, report: &mut AnalysisReport) {
1118 match stmt {
1119 JsStmt::For { body, span, .. } => {
1120 let new_depth = loop_depth + 1;
1121 if new_depth >= 3 {
1122 report.add_issue(IssueLevel::Warning, "performance-bottleneck".to_string(), "Deeply nested loops (3+ levels) may cause performance issues".to_string(), Some(*span));
1123 }
1124 check_nested_loops(body, new_depth, report);
1125 }
1126 JsStmt::While { body, span, .. } => {
1127 let new_depth = loop_depth + 1;
1128 if new_depth >= 3 {
1129 report.add_issue(IssueLevel::Warning, "performance-bottleneck".to_string(), "Deeply nested loops (3+ levels) may cause performance issues".to_string(), Some(*span));
1130 }
1131 check_nested_loops(body, new_depth, report);
1132 }
1133 JsStmt::Block(stmts, _, _) => {
1134 for s in stmts {
1135 check_nested_loops(s, loop_depth, report);
1136 }
1137 }
1138 JsStmt::If { consequent, alternate, .. } => {
1139 check_nested_loops(consequent, loop_depth, report);
1140 if let Some(alt) = alternate {
1141 check_nested_loops(alt, loop_depth, report);
1142 }
1143 }
1144 _ => {}
1145 }
1146 }
1147
1148 fn check_dom_operations(expr: &JsExpr, report: &mut AnalysisReport) {
1150 match expr {
1151 JsExpr::Call { callee, args, span, .. } => {
1152 if let JsExpr::Member { property, computed: false, .. } = &**callee {
1154 if let JsExpr::Identifier(prop_name, _, _) = &**property {
1155 if prop_name == "getElementById" || prop_name == "querySelector" || prop_name == "querySelectorAll" {
1156 report.add_issue(IssueLevel::Info, "performance-bottleneck".to_string(), "Frequent DOM queries may cause performance issues, consider caching results".to_string(), Some(*span));
1157 }
1158 }
1159 }
1160 check_dom_operations(callee, report);
1161 for arg in args {
1162 check_dom_operations(arg, report);
1163 }
1164 }
1165 JsExpr::Member { object, property, computed, .. } => {
1166 check_dom_operations(object, report);
1167 if *computed {
1168 check_dom_operations(property, report);
1169 }
1170 }
1171 JsExpr::Binary { left, right, .. } => {
1172 check_dom_operations(left, report);
1173 check_dom_operations(right, report);
1174 }
1175 JsExpr::Unary { argument, .. } => {
1176 check_dom_operations(argument, report);
1177 }
1178 JsExpr::Array(elements, _, _) => {
1179 for el in elements {
1180 check_dom_operations(el, report);
1181 }
1182 }
1183 JsExpr::Object(properties, _, _) => {
1184 for value in properties.values() {
1185 check_dom_operations(value, report);
1186 }
1187 }
1188 JsExpr::ArrowFunction { body, .. } => {
1189 check_dom_operations(body, report);
1190 }
1191 JsExpr::Conditional { test, consequent, alternate, .. } => {
1192 check_dom_operations(test, report);
1193 check_dom_operations(consequent, report);
1194 check_dom_operations(alternate, report);
1195 }
1196 JsExpr::TemplateLiteral { expressions, .. } => {
1197 for e in expressions {
1198 check_dom_operations(e, report);
1199 }
1200 }
1201 _ => {}
1202 }
1203 }
1204
1205 fn check_dom_operations_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
1206 match stmt {
1207 JsStmt::Expr(expr, _, _) => check_dom_operations(expr, report),
1208 JsStmt::VariableDecl { init, .. } => {
1209 if let Some(e) = init {
1210 check_dom_operations(e, report);
1211 }
1212 }
1213 JsStmt::Return(expr, _, _) => {
1214 if let Some(e) = expr {
1215 check_dom_operations(e, report);
1216 }
1217 }
1218 JsStmt::If { test, consequent, alternate, .. } => {
1219 check_dom_operations(test, report);
1220 check_dom_operations_in_stmt(consequent, report);
1221 if let Some(alt) = alternate {
1222 check_dom_operations_in_stmt(alt, report);
1223 }
1224 }
1225 JsStmt::While { test, body, .. } => {
1226 check_dom_operations(test, report);
1227 check_dom_operations_in_stmt(body, report);
1228 }
1229 JsStmt::For { init, test, update, body, .. } => {
1230 if let Some(i) = init {
1231 check_dom_operations_in_stmt(i, report);
1232 }
1233 if let Some(t) = test {
1234 check_dom_operations(t, report);
1235 }
1236 if let Some(u) = update {
1237 check_dom_operations(u, report);
1238 }
1239 check_dom_operations_in_stmt(body, report);
1240 }
1241 JsStmt::Block(stmts, _, _) => {
1242 for s in stmts {
1243 check_dom_operations_in_stmt(s, report);
1244 }
1245 }
1246 _ => {}
1247 }
1248 }
1249
1250 fn check_large_array_operations(expr: &JsExpr, report: &mut AnalysisReport) {
1252 match expr {
1253 JsExpr::Call { callee, args, span, .. } => {
1254 if let JsExpr::Member { property, computed: false, .. } = &**callee {
1256 if let JsExpr::Identifier(prop_name, _, _) = &**property {
1257 if prop_name == "map" || prop_name == "filter" || prop_name == "reduce" || prop_name == "forEach" {
1258 report.add_issue(IssueLevel::Info, "performance-bottleneck".to_string(), "Large array operations may cause performance issues, consider using more efficient methods".to_string(), Some(*span));
1259 }
1260 }
1261 }
1262 check_large_array_operations(callee, report);
1263 for arg in args {
1264 check_large_array_operations(arg, report);
1265 }
1266 }
1267 JsExpr::Member { object, property, computed, .. } => {
1268 check_large_array_operations(object, report);
1269 if *computed {
1270 check_large_array_operations(property, report);
1271 }
1272 }
1273 JsExpr::Binary { left, right, .. } => {
1274 check_large_array_operations(left, report);
1275 check_large_array_operations(right, report);
1276 }
1277 JsExpr::Unary { argument, .. } => {
1278 check_large_array_operations(argument, report);
1279 }
1280 JsExpr::Array(elements, span, _) => {
1281 if elements.len() > 100 {
1282 report.add_issue(IssueLevel::Warning, "performance-bottleneck".to_string(), "Large array literals may cause performance issues".to_string(), Some(*span));
1283 }
1284 for el in elements {
1285 check_large_array_operations(el, report);
1286 }
1287 }
1288 JsExpr::Object(properties, _, _) => {
1289 for value in properties.values() {
1290 check_large_array_operations(value, report);
1291 }
1292 }
1293 JsExpr::ArrowFunction { body, .. } => {
1294 check_large_array_operations(body, report);
1295 }
1296 JsExpr::Conditional { test, consequent, alternate, .. } => {
1297 check_large_array_operations(test, report);
1298 check_large_array_operations(consequent, report);
1299 check_large_array_operations(alternate, report);
1300 }
1301 JsExpr::TemplateLiteral { expressions, .. } => {
1302 for e in expressions {
1303 check_large_array_operations(e, report);
1304 }
1305 }
1306 _ => {}
1307 }
1308 }
1309
1310 fn check_large_array_operations_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
1311 match stmt {
1312 JsStmt::Expr(expr, _, _) => check_large_array_operations(expr, report),
1313 JsStmt::VariableDecl { init, .. } => {
1314 if let Some(e) = init {
1315 check_large_array_operations(e, report);
1316 }
1317 }
1318 JsStmt::Return(expr, _, _) => {
1319 if let Some(e) = expr {
1320 check_large_array_operations(e, report);
1321 }
1322 }
1323 JsStmt::If { test, consequent, alternate, .. } => {
1324 check_large_array_operations(test, report);
1325 check_large_array_operations_in_stmt(consequent, report);
1326 if let Some(alt) = alternate {
1327 check_large_array_operations_in_stmt(alt, report);
1328 }
1329 }
1330 JsStmt::While { test, body, .. } => {
1331 check_large_array_operations(test, report);
1332 check_large_array_operations_in_stmt(body, report);
1333 }
1334 JsStmt::For { init, test, update, body, .. } => {
1335 if let Some(i) = init {
1336 check_large_array_operations_in_stmt(i, report);
1337 }
1338 if let Some(t) = test {
1339 check_large_array_operations(t, report);
1340 }
1341 if let Some(u) = update {
1342 check_large_array_operations(u, report);
1343 }
1344 check_large_array_operations_in_stmt(body, report);
1345 }
1346 JsStmt::Block(stmts, _, _) => {
1347 for s in stmts {
1348 check_large_array_operations_in_stmt(s, report);
1349 }
1350 }
1351 _ => {}
1352 }
1353 }
1354
1355 for stmt in &program.body {
1357 check_nested_loops(stmt, 0, report);
1358 check_dom_operations_in_stmt(stmt, report);
1359 check_large_array_operations_in_stmt(stmt, report);
1360 }
1361 }
1362}
1363
1364pub struct SecurityRule;
1366
1367impl Rule for SecurityRule {
1368 fn code(&self) -> String {
1369 "security".to_string()
1370 }
1371
1372 fn description(&self) -> String {
1373 "检查安全问题".to_string()
1374 }
1375
1376 fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
1377 fn check_sql_injection(expr: &JsExpr, report: &mut AnalysisReport) {
1379 match expr {
1380 JsExpr::Binary { left, right, op, span, .. } => {
1381 if *op == "+" {
1383 let mut has_sql_keyword = false;
1385 let mut has_user_input = false;
1386
1387 fn check_sql_keywords(expr: &JsExpr) -> bool {
1388 match expr {
1389 JsExpr::Literal(NargoValue::String(s), _, _) => {
1390 let s_lower = s.to_lowercase();
1391 s_lower.contains("select") || s_lower.contains("insert") || s_lower.contains("update") || s_lower.contains("delete") || s_lower.contains("from") || s_lower.contains("where")
1392 }
1393 JsExpr::Binary { left, right, .. } => check_sql_keywords(left) || check_sql_keywords(right),
1394 JsExpr::Identifier(_, _, _) => true, _ => false,
1396 }
1397 }
1398
1399 has_sql_keyword = check_sql_keywords(left) || check_sql_keywords(right);
1400 has_user_input = matches!(&**left, JsExpr::Identifier(_, _, _)) || matches!(&**right, JsExpr::Identifier(_, _, _));
1401
1402 if has_sql_keyword && has_user_input {
1403 report.add_issue(IssueLevel::Error, "security".to_string(), "Potential SQL injection vulnerability: avoid string concatenation for SQL queries".to_string(), Some(*span));
1404 }
1405 }
1406 check_sql_injection(left, report);
1407 check_sql_injection(right, report);
1408 }
1409 JsExpr::Call { callee, args, .. } => {
1410 check_sql_injection(callee, report);
1411 for arg in args {
1412 check_sql_injection(arg, report);
1413 }
1414 }
1415 JsExpr::Member { object, property, computed, .. } => {
1416 check_sql_injection(object, report);
1417 if *computed {
1418 check_sql_injection(property, report);
1419 }
1420 }
1421 JsExpr::Array(elements, _, _) => {
1422 for el in elements {
1423 check_sql_injection(el, report);
1424 }
1425 }
1426 JsExpr::Object(properties, _, _) => {
1427 for value in properties.values() {
1428 check_sql_injection(value, report);
1429 }
1430 }
1431 JsExpr::ArrowFunction { body, .. } => {
1432 check_sql_injection(body, report);
1433 }
1434 JsExpr::Conditional { test, consequent, alternate, .. } => {
1435 check_sql_injection(test, report);
1436 check_sql_injection(consequent, report);
1437 check_sql_injection(alternate, report);
1438 }
1439 JsExpr::TemplateLiteral { expressions, .. } => {
1440 for e in expressions {
1441 check_sql_injection(e, report);
1442 }
1443 }
1444 _ => {}
1445 }
1446 }
1447
1448 fn check_xss(expr: &JsExpr, report: &mut AnalysisReport) {
1450 match expr {
1451 JsExpr::Member { object, property, computed: false, span, .. } => {
1452 if let JsExpr::Identifier(prop_name, _, _) = &**property {
1453 if prop_name == "innerHTML" {
1455 report.add_issue(IssueLevel::Warning, "security".to_string(), "Potential XSS vulnerability: avoid using innerHTML with untrusted data".to_string(), Some(*span));
1456 }
1457 }
1458 check_xss(object, report);
1459 }
1460 JsExpr::Call { callee, args, span, .. } => {
1461 if let JsExpr::Member { property, computed: false, .. } = &**callee {
1463 if let JsExpr::Identifier(prop_name, _, _) = &**property {
1464 if prop_name == "write" {
1465 report.add_issue(IssueLevel::Warning, "security".to_string(), "Potential XSS vulnerability: avoid using document.write".to_string(), Some(*span));
1466 }
1467 }
1468 }
1469 check_xss(callee, report);
1470 for arg in args {
1471 check_xss(arg, report);
1472 }
1473 }
1474 JsExpr::Binary { left, right, .. } => {
1475 check_xss(left, report);
1476 check_xss(right, report);
1477 }
1478 JsExpr::Unary { argument, .. } => {
1479 check_xss(argument, report);
1480 }
1481 JsExpr::Array(elements, _, _) => {
1482 for el in elements {
1483 check_xss(el, report);
1484 }
1485 }
1486 JsExpr::Object(properties, _, _) => {
1487 for value in properties.values() {
1488 check_xss(value, report);
1489 }
1490 }
1491 JsExpr::ArrowFunction { body, .. } => {
1492 check_xss(body, report);
1493 }
1494 JsExpr::Conditional { test, consequent, alternate, .. } => {
1495 check_xss(test, report);
1496 check_xss(consequent, report);
1497 check_xss(alternate, report);
1498 }
1499 JsExpr::TemplateLiteral { expressions, .. } => {
1500 for e in expressions {
1501 check_xss(e, report);
1502 }
1503 }
1504 _ => {}
1505 }
1506 }
1507
1508 fn check_password_storage(expr: &JsExpr, report: &mut AnalysisReport) {
1510 match expr {
1511 JsExpr::Call { callee, args, span, .. } => {
1512 if let JsExpr::Identifier(name, _, _) = &**callee {
1514 if name == "localStorage" || name == "sessionStorage" {
1515 for arg in args {
1517 if let JsExpr::Literal(NargoValue::String(s), _, _) = arg {
1518 let s_lower = s.to_lowercase();
1519 if s_lower.contains("password") || s_lower.contains("pwd") {
1520 report.add_issue(IssueLevel::Error, "security".to_string(), "Insecure password storage: avoid storing passwords in localStorage/sessionStorage".to_string(), Some(*span));
1521 }
1522 }
1523 }
1524 }
1525 }
1526 check_password_storage(callee, report);
1527 for arg in args {
1528 check_password_storage(arg, report);
1529 }
1530 }
1531 JsExpr::Member { object, property, computed, .. } => {
1532 check_password_storage(object, report);
1533 if *computed {
1534 check_password_storage(property, report);
1535 }
1536 }
1537 JsExpr::Binary { left, right, .. } => {
1538 check_password_storage(left, report);
1539 check_password_storage(right, report);
1540 }
1541 JsExpr::Unary { argument, .. } => {
1542 check_password_storage(argument, report);
1543 }
1544 JsExpr::Array(elements, _, _) => {
1545 for el in elements {
1546 check_password_storage(el, report);
1547 }
1548 }
1549 JsExpr::Object(properties, _, _) => {
1550 for value in properties.values() {
1551 check_password_storage(value, report);
1552 }
1553 }
1554 JsExpr::ArrowFunction { body, .. } => {
1555 check_password_storage(body, report);
1556 }
1557 JsExpr::Conditional { test, consequent, alternate, .. } => {
1558 check_password_storage(test, report);
1559 check_password_storage(consequent, report);
1560 check_password_storage(alternate, report);
1561 }
1562 JsExpr::TemplateLiteral { expressions, .. } => {
1563 for e in expressions {
1564 check_password_storage(e, report);
1565 }
1566 }
1567 _ => {}
1568 }
1569 }
1570
1571 fn check_security_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
1572 match stmt {
1573 JsStmt::Expr(expr, _, _) => {
1574 check_sql_injection(expr, report);
1575 check_xss(expr, report);
1576 check_password_storage(expr, report);
1577 }
1578 JsStmt::VariableDecl { init, .. } => {
1579 if let Some(e) = init {
1580 check_sql_injection(e, report);
1581 check_xss(e, report);
1582 check_password_storage(e, report);
1583 }
1584 }
1585 JsStmt::Return(expr, _, _) => {
1586 if let Some(e) = expr {
1587 check_sql_injection(e, report);
1588 check_xss(e, report);
1589 check_password_storage(e, report);
1590 }
1591 }
1592 JsStmt::If { test, consequent, alternate, .. } => {
1593 check_sql_injection(test, report);
1594 check_xss(test, report);
1595 check_password_storage(test, report);
1596 check_security_in_stmt(consequent, report);
1597 if let Some(alt) = alternate {
1598 check_security_in_stmt(alt, report);
1599 }
1600 }
1601 JsStmt::While { test, body, .. } => {
1602 check_sql_injection(test, report);
1603 check_xss(test, report);
1604 check_password_storage(test, report);
1605 check_security_in_stmt(body, report);
1606 }
1607 JsStmt::For { init, test, update, body, .. } => {
1608 if let Some(i) = init {
1609 check_security_in_stmt(i, report);
1610 }
1611 if let Some(t) = test {
1612 check_sql_injection(t, report);
1613 check_xss(t, report);
1614 check_password_storage(t, report);
1615 }
1616 if let Some(u) = update {
1617 check_sql_injection(u, report);
1618 check_xss(u, report);
1619 check_password_storage(u, report);
1620 }
1621 check_security_in_stmt(body, report);
1622 }
1623 JsStmt::Block(stmts, _, _) => {
1624 for s in stmts {
1625 check_security_in_stmt(s, report);
1626 }
1627 }
1628 _ => {}
1629 }
1630 }
1631
1632 for stmt in &program.body {
1634 check_security_in_stmt(stmt, report);
1635 }
1636 }
1637}
1638
1639pub struct CodeStyleRule;
1641
1642impl Rule for CodeStyleRule {
1643 fn code(&self) -> String {
1644 "code-style".to_string()
1645 }
1646
1647 fn description(&self) -> String {
1648 "检查代码风格问题".to_string()
1649 }
1650
1651 fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
1652 fn check_naming_conventions(stmt: &JsStmt, report: &mut AnalysisReport) {
1654 match stmt {
1655 JsStmt::VariableDecl { id, span, .. } => {
1656 if id.starts_with('[') && id.ends_with(']') {
1658 let content = &id[1..id.len() - 1];
1659 for part in content.split(',') {
1660 let trimmed = part.trim();
1661 if !trimmed.is_empty() {
1662 check_variable_name(trimmed, *span, report);
1663 }
1664 }
1665 }
1666 else {
1667 check_variable_name(id, *span, report);
1668 }
1669 }
1670 JsStmt::FunctionDecl { id, span, .. } => {
1671 if !id.starts_with(|c: char| c.is_ascii_lowercase()) {
1673 report.add_issue(IssueLevel::Warning, "code-style".to_string(), format!("Function name '{}' should use camelCase", id), Some(*span));
1674 }
1675 }
1676 JsStmt::Block(stmts, _, _) => {
1677 for s in stmts {
1678 check_naming_conventions(s, report);
1679 }
1680 }
1681 JsStmt::If { consequent, alternate, .. } => {
1682 check_naming_conventions(consequent, report);
1683 if let Some(alt) = alternate {
1684 check_naming_conventions(alt, report);
1685 }
1686 }
1687 JsStmt::While { body, .. } => {
1688 check_naming_conventions(body, report);
1689 }
1690 JsStmt::For { body, .. } => {
1691 check_naming_conventions(body, report);
1692 }
1693 _ => {}
1694 }
1695 }
1696
1697 fn check_variable_name(name: &str, span: Span, report: &mut AnalysisReport) {
1698 if name.starts_with(|c: char| c.is_ascii_uppercase()) {
1700 report.add_issue(IssueLevel::Warning, "code-style".to_string(), format!("Variable name '{}' should use camelCase", name), Some(span));
1701 }
1702 if name.len() == 1 && !"ijklmn".contains(name) {
1704 report.add_issue(IssueLevel::Info, "code-style".to_string(), format!("Variable name '{}' is too short, consider using a more descriptive name", name), Some(span));
1705 }
1706 }
1707
1708 fn check_code_length(stmt: &JsStmt, report: &mut AnalysisReport) {
1710 match stmt {
1711 JsStmt::FunctionDecl { body, span, .. } => {
1712 let body_length = body.len();
1713 if body_length > 50 {
1714 report.add_issue(IssueLevel::Warning, "code-style".to_string(), "Function is too long (over 50 statements), consider refactoring".to_string(), Some(*span));
1715 }
1716 }
1717 JsStmt::Block(stmts, span, ..) => {
1718 let block_length = stmts.len();
1719 if block_length > 30 {
1720 report.add_issue(IssueLevel::Info, "code-style".to_string(), "Block is too long (over 30 statements), consider refactoring".to_string(), Some(*span));
1721 }
1722 }
1723 JsStmt::If { consequent, alternate, .. } => {
1724 check_code_length(consequent, report);
1725 if let Some(alt) = alternate {
1726 check_code_length(alt, report);
1727 }
1728 }
1729 JsStmt::While { body, .. } => {
1730 check_code_length(body, report);
1731 }
1732 JsStmt::For { body, .. } => {
1733 check_code_length(body, report);
1734 }
1735 _ => {}
1736 }
1737 }
1738
1739 fn count_statements(stmt: &JsStmt) -> usize {
1740 match stmt {
1741 JsStmt::Block(stmts, _, _) => stmts.len(),
1742 _ => 1,
1743 }
1744 }
1745
1746 fn check_whitespace(stmt: &JsStmt, report: &mut AnalysisReport) {
1748 }
1751
1752 for stmt in &program.body {
1754 check_naming_conventions(stmt, report);
1755 check_code_length(stmt, report);
1756 check_whitespace(stmt, report);
1757 }
1758 }
1759}
1760
1761#[derive(Default)]
1762pub struct ScriptAnalyzer;
1763
1764impl ScriptAnalyzer {
1765 pub fn new() -> Self {
1766 Self
1767 }
1768
1769 pub fn analyze(&self, program: &JsProgram) -> Result<ScriptMetadata> {
1770 let mut meta = ScriptMetadata::default();
1771
1772 let mut declared_vars = HashSet::new();
1774 let mut declared_functions = HashSet::new();
1775
1776 for stmt in &program.body {
1778 match stmt {
1779 JsStmt::VariableDecl { id, init, .. } => {
1780 self.analyze_variable_decl(id, init.as_ref(), &mut meta);
1781 if id.starts_with('[') && id.ends_with(']') {
1783 let content = &id[1..id.len() - 1];
1784 for part in content.split(',') {
1785 let trimmed = part.trim();
1786 if !trimmed.is_empty() {
1787 declared_vars.insert(trimmed.to_string());
1788 }
1789 }
1790 }
1791 else {
1792 declared_vars.insert(id.to_string());
1793 }
1794 }
1795 JsStmt::FunctionDecl { id, params: _, body: _, .. } => {
1796 meta.actions.insert(id.clone());
1797 declared_functions.insert(id.clone());
1798 }
1799 JsStmt::Expr(expr, _, _) => {
1800 self.analyze_expression(expr, &mut meta);
1801 }
1802 _ => {}
1803 }
1804 }
1805
1806 for stmt in &program.body {
1808 match stmt {
1809 JsStmt::VariableDecl { id, init, .. } => {
1810 if let Some(init_expr) = init {
1811 let mut deps = HashSet::new();
1812 self.find_dependencies(init_expr, &mut deps, &meta);
1813 if !deps.is_empty() {
1814 meta.dependencies.insert(id.clone(), deps);
1815 }
1816 }
1817 }
1818 JsStmt::FunctionDecl { id, body, .. } => {
1819 let mut deps = HashSet::new();
1820 for s in body {
1821 self.find_dependencies_in_stmt(s, &mut deps, &meta);
1822 }
1823 if !deps.is_empty() {
1824 meta.dependencies.insert(id.clone(), deps);
1825 }
1826 }
1827 JsStmt::Expr(expr, _, _) => {
1828 if let JsExpr::Call { callee, args, .. } = expr {
1830 if let JsExpr::Identifier(name, _, _) = &**callee {
1831 if name == "$effect" || name == "watchEffect" {
1832 if let Some(first_arg) = args.get(0) {
1833 let mut deps = HashSet::new();
1834 self.find_dependencies(first_arg, &mut deps, &meta);
1835 if !deps.is_empty() {
1836 meta.dependencies.insert(format!("$effect_{}", meta.dependencies.len()), deps);
1838 }
1839 }
1840 }
1841 }
1842 }
1843 }
1844 _ => {}
1845 }
1846 }
1847
1848 Ok(meta)
1849 }
1850
1851 pub fn analyze_with_rules(&self, program: &JsProgram, file_path: Option<String>) -> Result<(ScriptMetadata, AnalysisReport)> {
1853 let start_time = std::time::Instant::now();
1854
1855 let meta = self.analyze(program)?;
1857
1858 let mut report = AnalysisReport::new(file_path);
1860
1861 let rule_engine = default_rule_engine();
1863
1864 rule_engine.run(program, &meta, &mut report);
1866
1867 let duration = start_time.elapsed();
1869 report.duration_ms = duration.as_millis() as u64;
1870
1871 report.sort_issues();
1873
1874 Ok((meta, report))
1875 }
1876
1877 fn analyze_variable_decl(&self, id: &str, init: Option<&JsExpr>, meta: &mut ScriptMetadata) {
1878 let mut ids = Vec::new();
1879 if id.starts_with('[') && id.ends_with(']') {
1880 let content = &id[1..id.len() - 1];
1882 for part in content.split(',') {
1883 let trimmed = part.trim();
1884 if !trimmed.is_empty() {
1885 ids.push(trimmed.to_string());
1886 }
1887 }
1888 }
1889 else {
1890 ids.push(id.to_string());
1891 }
1892
1893 if id == "props" {
1895 if let Some(JsExpr::Call { callee, args, .. }) = init {
1896 if let JsExpr::Identifier(name, _, _) = &**callee {
1897 if name == "defineProps" {
1898 self.extract_keys_from_args(args, &mut meta.props);
1899 return;
1900 }
1901 }
1902 }
1903 }
1904
1905 if id == "emit" || id == "emits" {
1907 if let Some(JsExpr::Call { callee, args, .. }) = init {
1908 if let JsExpr::Identifier(name, _, _) = &**callee {
1909 if name == "defineEmits" {
1910 self.extract_keys_from_args(args, &mut meta.emits);
1911 return;
1912 }
1913 }
1914 }
1915 }
1916
1917 if let Some(init_expr) = init {
1919 match init_expr {
1920 JsExpr::Call { callee, .. } => {
1921 if let JsExpr::Identifier(name, _, _) = &**callee {
1922 if name == "signal" || name == "createSignal" || name == "ref" || name == "reactive" {
1923 for (i, var_id) in ids.iter().enumerate() {
1924 if i == 0 || name == "ref" || name == "reactive" {
1925 meta.signals.insert(var_id.clone());
1926 }
1927 }
1928 }
1929 else if name == "computed" || name == "$computed" || name == "createComputed" {
1930 for var_id in &ids {
1931 meta.computed.insert(var_id.clone());
1932 }
1933 }
1934 }
1935 }
1936 JsExpr::ArrowFunction { .. } => {
1937 for var_id in &ids {
1938 meta.actions.insert(var_id.clone());
1939 }
1940 }
1941 _ => {}
1942 }
1943 }
1944 }
1945
1946 fn analyze_expression(&self, expr: &JsExpr, meta: &mut ScriptMetadata) {
1947 if let JsExpr::Call { callee, args, .. } = expr {
1948 if let JsExpr::Identifier(name, _, _) = &**callee {
1949 match name.as_str() {
1950 "defineProps" => {
1951 self.extract_keys_from_args(args, &mut meta.props);
1952 }
1953 "defineEmits" => {
1954 self.extract_keys_from_args(args, &mut meta.emits);
1955 }
1956 _ => {}
1957 }
1958 }
1959 }
1960 }
1961
1962 fn find_dependencies(&self, expr: &JsExpr, deps: &mut HashSet<String>, meta: &ScriptMetadata) {
1963 match expr {
1964 JsExpr::Identifier(name, _, _) => {
1965 if meta.signals.contains(name) || meta.computed.contains(name) || meta.props.contains(name) {
1966 deps.insert(name.clone());
1967 }
1968 }
1969 JsExpr::Binary { left, right, .. } => {
1970 self.find_dependencies(left, deps, meta);
1971 self.find_dependencies(right, deps, meta);
1972 }
1973 JsExpr::Unary { argument, .. } => {
1974 self.find_dependencies(argument, deps, meta);
1975 }
1976 JsExpr::Call { callee, args, .. } => {
1977 self.find_dependencies(callee, deps, meta);
1978 for arg in args {
1979 self.find_dependencies(arg, deps, meta);
1980 }
1981 }
1982 JsExpr::Member { object, property, computed, .. } => {
1983 self.find_dependencies(object, deps, meta);
1984 if *computed {
1985 self.find_dependencies(property, deps, meta);
1986 }
1987 }
1988 JsExpr::Array(elements, _, _) => {
1989 for el in elements {
1990 self.find_dependencies(el, deps, meta);
1991 }
1992 }
1993 JsExpr::Object(properties, _, _) => {
1994 for value in properties.values() {
1995 self.find_dependencies(value, deps, meta);
1996 }
1997 }
1998 JsExpr::ArrowFunction { body, .. } => {
1999 self.find_dependencies(body, deps, meta);
2000 }
2001 JsExpr::Conditional { test, consequent, alternate, .. } => {
2002 self.find_dependencies(test, deps, meta);
2003 self.find_dependencies(consequent, deps, meta);
2004 self.find_dependencies(alternate, deps, meta);
2005 }
2006 JsExpr::TemplateLiteral { expressions, .. } => {
2007 for e in expressions {
2008 self.find_dependencies(e, deps, meta);
2009 }
2010 }
2011 _ => {}
2012 }
2013 }
2014
2015 fn find_dependencies_in_stmt(&self, stmt: &JsStmt, deps: &mut HashSet<String>, meta: &ScriptMetadata) {
2016 match stmt {
2017 JsStmt::Expr(expr, _, _) => self.find_dependencies(expr, deps, meta),
2018 JsStmt::VariableDecl { init, .. } => {
2019 if let Some(e) = init {
2020 self.find_dependencies(e, deps, meta);
2021 }
2022 }
2023 JsStmt::Return(expr, _, _) => {
2024 if let Some(e) = expr {
2025 self.find_dependencies(e, deps, meta);
2026 }
2027 }
2028 JsStmt::If { test, consequent, alternate, .. } => {
2029 self.find_dependencies(test, deps, meta);
2030 self.find_dependencies_in_stmt(consequent, deps, meta);
2031 if let Some(alt) = alternate {
2032 self.find_dependencies_in_stmt(alt, deps, meta);
2033 }
2034 }
2035 JsStmt::While { test, body, .. } => {
2036 self.find_dependencies(test, deps, meta);
2037 self.find_dependencies_in_stmt(body, deps, meta);
2038 }
2039 JsStmt::For { init, test, update, body, .. } => {
2040 if let Some(i) = init {
2041 self.find_dependencies_in_stmt(i, deps, meta);
2042 }
2043 if let Some(t) = test {
2044 self.find_dependencies(t, deps, meta);
2045 }
2046 if let Some(u) = update {
2047 self.find_dependencies(u, deps, meta);
2048 }
2049 self.find_dependencies_in_stmt(body, deps, meta);
2050 }
2051 JsStmt::Block(stmts, _, _) => {
2052 for s in stmts {
2053 self.find_dependencies_in_stmt(s, deps, meta);
2054 }
2055 }
2056 _ => {}
2057 }
2058 }
2059
2060 fn extract_keys_from_args(&self, args: &[JsExpr], set: &mut HashSet<String>) {
2061 if let Some(first_arg) = args.get(0) {
2062 match first_arg {
2063 JsExpr::Object(map, _, _) => {
2064 for key in map.keys() {
2065 set.insert(key.clone());
2066 }
2067 }
2068 JsExpr::Array(arr, _, _) => {
2069 for item in arr {
2070 if let JsExpr::Literal(NargoValue::String(s), _, _) = item {
2071 set.insert(s.clone());
2072 }
2073 }
2074 }
2075 _ => {}
2076 }
2077 }
2078 }
2079}
2080
2081pub fn default_rule_engine() -> RuleEngine {
2083 let mut engine = RuleEngine::new();
2084 engine.add_rule(Box::new(UnusedVariableRule));
2085 engine.add_rule(Box::new(UndefinedVariableRule));
2086 engine.add_rule(Box::new(UnsafeOperationRule));
2087 engine.add_rule(Box::new(UnusedImportRule));
2088 engine.add_rule(Box::new(MemoryLeakRule));
2089 engine.add_rule(Box::new(PerformanceBottleneckRule));
2090 engine.add_rule(Box::new(SecurityRule));
2091 engine.add_rule(Box::new(CodeStyleRule));
2092 engine
2093}