#![warn(missing_docs)]
use nargo_ir::{JsExpr, JsProgram, JsStmt};
use nargo_types::{NargoValue, Result, Span};
use std::{
collections::{HashMap, HashSet},
fs::File,
io::Write,
path::Path,
};
#[derive(Debug, Clone, Default)]
pub struct ScriptMetadata {
pub signals: HashSet<String>,
pub computed: HashSet<String>,
pub props: HashSet<String>,
pub emits: HashSet<String>,
pub actions: HashSet<String>,
pub dependencies: HashMap<String, HashSet<String>>, }
impl ScriptMetadata {
pub fn to_nargo_value(&self) -> NargoValue {
let mut map = HashMap::new();
let signals_arr = self.signals.iter().map(|s| NargoValue::String(s.clone())).collect();
map.insert("signals".to_string(), NargoValue::Array(signals_arr));
let computed_arr = self.computed.iter().map(|s| NargoValue::String(s.clone())).collect();
map.insert("computed".to_string(), NargoValue::Array(computed_arr));
let props_arr = self.props.iter().map(|s| NargoValue::String(s.clone())).collect();
map.insert("props".to_string(), NargoValue::Array(props_arr));
let emits_arr = self.emits.iter().map(|s| NargoValue::String(s.clone())).collect();
map.insert("emits".to_string(), NargoValue::Array(emits_arr));
let actions_arr = self.actions.iter().map(|s| NargoValue::String(s.clone())).collect();
map.insert("actions".to_string(), NargoValue::Array(actions_arr));
let mut deps_map = HashMap::new();
for (k, v) in &self.dependencies {
let deps_arr = v.iter().map(|s| NargoValue::String(s.clone())).collect();
deps_map.insert(k.clone(), NargoValue::Array(deps_arr));
}
map.insert("dependencies".to_string(), NargoValue::Object(deps_map));
NargoValue::Object(map)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueLevel {
Error,
Warning,
Info,
}
#[derive(Debug, Clone)]
pub struct AnalysisIssue {
pub level: IssueLevel,
pub code: String,
pub message: String,
pub span: Option<Span>,
}
#[derive(Debug, Clone, Default)]
pub struct AnalysisReport {
pub file_path: Option<String>,
pub issues: Vec<AnalysisIssue>,
pub duration_ms: u64,
}
impl AnalysisReport {
pub fn new(file_path: Option<String>) -> Self {
Self { file_path, issues: Vec::new(), duration_ms: 0 }
}
pub fn add_issue(&mut self, level: IssueLevel, code: String, message: String, span: Option<Span>) {
self.issues.push(AnalysisIssue { level, code, message, span });
}
pub fn sort_issues(&mut self) {
self.issues.sort_by(|a, b| a.level.cmp(&b.level));
}
pub fn generate_console_report(&self) -> String {
let mut report = String::new();
if let Some(file_path) = &self.file_path {
report.push_str(&format!(
"Analysis report for: {}
",
file_path
));
}
else {
report.push_str(
"Analysis report
",
);
}
report.push_str(&format!(
"Duration: {}ms
",
self.duration_ms
));
report.push_str(&format!(
"Total issues: {}
",
self.issues.len()
));
let error_count = self.issues.iter().filter(|i| i.level == IssueLevel::Error).count();
let warning_count = self.issues.iter().filter(|i| i.level == IssueLevel::Warning).count();
let info_count = self.issues.iter().filter(|i| i.level == IssueLevel::Info).count();
report.push_str(&format!(
"Errors: {}, Warnings: {}, Info: {}
",
error_count, warning_count, info_count
));
for (i, issue) in self.issues.iter().enumerate() {
let level_str = match issue.level {
IssueLevel::Error => "ERROR",
IssueLevel::Warning => "WARNING",
IssueLevel::Info => "INFO",
};
report.push_str(&format!(
"{}. [{}] [{}] {}
",
i + 1,
level_str,
issue.code,
issue.message
));
if let Some(span) = &issue.span {
report.push_str(&format!(
" Location: line {}, column {}
",
span.start.line, span.start.column
));
}
report.push_str("\n");
}
report
}
pub fn generate_json_report(&self) -> Result<NargoValue> {
let mut issues = Vec::new();
for issue in &self.issues {
let mut issue_map = HashMap::new();
issue_map.insert(
"level".to_string(),
NargoValue::String(match issue.level {
IssueLevel::Error => "error".to_string(),
IssueLevel::Warning => "warning".to_string(),
IssueLevel::Info => "info".to_string(),
}),
);
issue_map.insert("code".to_string(), NargoValue::String(issue.code.clone()));
issue_map.insert("message".to_string(), NargoValue::String(issue.message.clone()));
if let Some(span) = &issue.span {
let mut span_map = HashMap::new();
span_map.insert("start_line".to_string(), NargoValue::Number(span.start.line as f64));
span_map.insert("start_column".to_string(), NargoValue::Number(span.start.column as f64));
span_map.insert("end_line".to_string(), NargoValue::Number(span.end.line as f64));
span_map.insert("end_column".to_string(), NargoValue::Number(span.end.column as f64));
issue_map.insert("span".to_string(), NargoValue::Object(span_map));
}
issues.push(NargoValue::Object(issue_map));
}
let mut report_map = HashMap::new();
if let Some(file_path) = &self.file_path {
report_map.insert("file_path".to_string(), NargoValue::String(file_path.clone()));
}
report_map.insert("issues".to_string(), NargoValue::Array(issues));
report_map.insert("duration_ms".to_string(), NargoValue::Number(self.duration_ms as f64));
report_map.insert("total_issues".to_string(), NargoValue::Number(self.issues.len() as f64));
Ok(NargoValue::Object(report_map))
}
pub fn save_to_file(&self, output_path: &str) -> Result<()> {
let json_report = self.generate_json_report()?;
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()))?;
let path = Path::new(output_path);
let mut file = File::create(path)?;
write!(file, "{}", report_str)?;
Ok(())
}
}
pub trait Rule {
fn code(&self) -> String;
fn description(&self) -> String;
fn check(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport);
}
#[derive(Default)]
pub struct RuleEngine {
rules: Vec<Box<dyn Rule>>,
}
impl RuleEngine {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn add_rule(&mut self, rule: Box<dyn Rule>) {
self.rules.push(rule);
}
pub fn run(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport) {
for rule in &self.rules {
rule.check(program, meta, report);
}
}
}
pub struct UnusedVariableRule;
impl Rule for UnusedVariableRule {
fn code(&self) -> String {
"unused-variable".to_string()
}
fn description(&self) -> String {
"检查未使用的变量".to_string()
}
fn check(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport) {
let mut declared_vars = HashSet::new();
let mut used_vars = HashSet::new();
for stmt in &program.body {
match stmt {
JsStmt::VariableDecl { id, .. } => {
if id.starts_with('[') && id.ends_with(']') {
let content = &id[1..id.len() - 1];
for part in content.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
declared_vars.insert(trimmed.to_string());
}
}
}
else {
declared_vars.insert(id.to_string());
}
}
_ => {}
}
}
fn collect_used_vars(expr: &JsExpr, used_vars: &mut HashSet<String>) {
match expr {
JsExpr::Identifier(name, _, _) => {
used_vars.insert(name.clone());
}
JsExpr::Binary { left, right, .. } => {
collect_used_vars(left, used_vars);
collect_used_vars(right, used_vars);
}
JsExpr::Unary { argument, .. } => {
collect_used_vars(argument, used_vars);
}
JsExpr::Call { callee, args, .. } => {
collect_used_vars(callee, used_vars);
for arg in args {
collect_used_vars(arg, used_vars);
}
}
JsExpr::Member { object, property, computed, .. } => {
collect_used_vars(object, used_vars);
if *computed {
collect_used_vars(property, used_vars);
}
}
JsExpr::Array(elements, _, _) => {
for el in elements {
collect_used_vars(el, used_vars);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
collect_used_vars(value, used_vars);
}
}
JsExpr::ArrowFunction { body, .. } => {
collect_used_vars(body, used_vars);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
collect_used_vars(test, used_vars);
collect_used_vars(consequent, used_vars);
collect_used_vars(alternate, used_vars);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
collect_used_vars(e, used_vars);
}
}
_ => {}
}
}
fn collect_used_vars_in_stmt(stmt: &JsStmt, used_vars: &mut HashSet<String>) {
match stmt {
JsStmt::Expr(expr, _, _) => collect_used_vars(expr, used_vars),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
collect_used_vars(e, used_vars);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
collect_used_vars(e, used_vars);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
collect_used_vars(test, used_vars);
collect_used_vars_in_stmt(consequent, used_vars);
if let Some(alt) = alternate {
collect_used_vars_in_stmt(alt, used_vars);
}
}
JsStmt::While { test, body, .. } => {
collect_used_vars(test, used_vars);
collect_used_vars_in_stmt(body, used_vars);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
collect_used_vars_in_stmt(i, used_vars);
}
if let Some(t) = test {
collect_used_vars(t, used_vars);
}
if let Some(u) = update {
collect_used_vars(u, used_vars);
}
collect_used_vars_in_stmt(body, used_vars);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
collect_used_vars_in_stmt(s, used_vars);
}
}
_ => {}
}
}
for stmt in &program.body {
collect_used_vars_in_stmt(stmt, &mut used_vars);
}
for var in &declared_vars {
if !used_vars.contains(var) && !meta.signals.contains(var) && !meta.computed.contains(var) && !meta.actions.contains(var) {
report.add_issue(IssueLevel::Warning, self.code(), format!("Unused variable: {}", var), None);
}
}
}
}
pub struct UndefinedVariableRule;
impl Rule for UndefinedVariableRule {
fn code(&self) -> String {
"undefined-variable".to_string()
}
fn description(&self) -> String {
"检查使用未定义的变量".to_string()
}
fn check(&self, program: &JsProgram, meta: &ScriptMetadata, report: &mut AnalysisReport) {
let mut declared_vars = HashSet::new();
for stmt in &program.body {
match stmt {
JsStmt::VariableDecl { id, .. } => {
if id.starts_with('[') && id.ends_with(']') {
let content = &id[1..id.len() - 1];
for part in content.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
declared_vars.insert(trimmed.to_string());
}
}
}
else {
declared_vars.insert(id.to_string());
}
}
JsStmt::FunctionDecl { id, .. } => {
declared_vars.insert(id.clone());
}
_ => {}
}
}
fn check_undefined_vars(expr: &JsExpr, declared_vars: &HashSet<String>, meta: &ScriptMetadata, report: &mut AnalysisReport) {
match expr {
JsExpr::Identifier(name, span, _) => {
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('$') {
report.add_issue(IssueLevel::Error, "undefined-variable".to_string(), format!("Undefined variable: {}", name), Some(*span));
}
}
JsExpr::Binary { left, right, .. } => {
check_undefined_vars(left, declared_vars, meta, report);
check_undefined_vars(right, declared_vars, meta, report);
}
JsExpr::Unary { argument, .. } => {
check_undefined_vars(argument, declared_vars, meta, report);
}
JsExpr::Call { callee, args, .. } => {
check_undefined_vars(callee, declared_vars, meta, report);
for arg in args {
check_undefined_vars(arg, declared_vars, meta, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_undefined_vars(object, declared_vars, meta, report);
if *computed {
check_undefined_vars(property, declared_vars, meta, report);
}
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_undefined_vars(el, declared_vars, meta, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_undefined_vars(value, declared_vars, meta, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_undefined_vars(body, declared_vars, meta, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_undefined_vars(test, declared_vars, meta, report);
check_undefined_vars(consequent, declared_vars, meta, report);
check_undefined_vars(alternate, declared_vars, meta, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_undefined_vars(e, declared_vars, meta, report);
}
}
_ => {}
}
}
fn check_undefined_vars_in_stmt(stmt: &JsStmt, declared_vars: &HashSet<String>, meta: &ScriptMetadata, report: &mut AnalysisReport) {
match stmt {
JsStmt::Expr(expr, _, _) => check_undefined_vars(expr, declared_vars, meta, report),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_undefined_vars(e, declared_vars, meta, report);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_undefined_vars(e, declared_vars, meta, report);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_undefined_vars(test, declared_vars, meta, report);
check_undefined_vars_in_stmt(consequent, declared_vars, meta, report);
if let Some(alt) = alternate {
check_undefined_vars_in_stmt(alt, declared_vars, meta, report);
}
}
JsStmt::While { test, body, .. } => {
check_undefined_vars(test, declared_vars, meta, report);
check_undefined_vars_in_stmt(body, declared_vars, meta, report);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_undefined_vars_in_stmt(i, declared_vars, meta, report);
}
if let Some(t) = test {
check_undefined_vars(t, declared_vars, meta, report);
}
if let Some(u) = update {
check_undefined_vars(u, declared_vars, meta, report);
}
check_undefined_vars_in_stmt(body, declared_vars, meta, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_undefined_vars_in_stmt(s, declared_vars, meta, report);
}
}
_ => {}
}
}
for stmt in &program.body {
check_undefined_vars_in_stmt(stmt, &declared_vars, meta, report);
}
}
}
pub struct UnsafeOperationRule;
impl Rule for UnsafeOperationRule {
fn code(&self) -> String {
"unsafe-operation".to_string()
}
fn description(&self) -> String {
"检查不安全的操作".to_string()
}
fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
fn check_unsafe_operations(expr: &JsExpr, report: &mut AnalysisReport) {
match expr {
JsExpr::Binary { left, right, op, span, .. } => {
if *op == "/" {
if let JsExpr::Literal(NargoValue::Number(0.0), _, _) = &**right {
report.add_issue(IssueLevel::Error, "unsafe-operation".to_string(), "Division by zero".to_string(), Some(*span));
}
}
if *op == "==" || *op == "!=" {
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));
}
check_unsafe_operations(left, report);
check_unsafe_operations(right, report);
}
JsExpr::Call { callee, args, span, .. } => {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "eval" {
report.add_issue(IssueLevel::Warning, "unsafe-operation".to_string(), "Use of eval is potentially unsafe".to_string(), Some(*span));
}
else if name == "Function" {
report.add_issue(IssueLevel::Warning, "unsafe-operation".to_string(), "Use of Function constructor is potentially unsafe".to_string(), Some(*span));
}
}
check_unsafe_operations(callee, report);
for arg in args {
check_unsafe_operations(arg, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_unsafe_operations(object, report);
if *computed {
check_unsafe_operations(property, report);
}
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_unsafe_operations(el, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_unsafe_operations(value, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_unsafe_operations(body, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_unsafe_operations(test, report);
check_unsafe_operations(consequent, report);
check_unsafe_operations(alternate, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_unsafe_operations(e, report);
}
}
_ => {}
}
}
fn check_unsafe_operations_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
match stmt {
JsStmt::Expr(expr, _, _) => check_unsafe_operations(expr, report),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_unsafe_operations(e, report);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_unsafe_operations(e, report);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_unsafe_operations(test, report);
check_unsafe_operations_in_stmt(consequent, report);
if let Some(alt) = alternate {
check_unsafe_operations_in_stmt(alt, report);
}
}
JsStmt::While { test, body, .. } => {
check_unsafe_operations(test, report);
check_unsafe_operations_in_stmt(body, report);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_unsafe_operations_in_stmt(i, report);
}
if let Some(t) = test {
check_unsafe_operations(t, report);
}
if let Some(u) = update {
check_unsafe_operations(u, report);
}
check_unsafe_operations_in_stmt(body, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_unsafe_operations_in_stmt(s, report);
}
}
_ => {}
}
}
for stmt in &program.body {
check_unsafe_operations_in_stmt(stmt, report);
}
}
}
pub struct UnusedImportRule;
impl Rule for UnusedImportRule {
fn code(&self) -> String {
"unused-import".to_string()
}
fn description(&self) -> String {
"检查未使用的导入".to_string()
}
fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
let mut imports = HashSet::new();
let mut used_imports = HashSet::new();
for stmt in &program.body {
match stmt {
JsStmt::Import { specifiers, source: _, span: _, trivia: _ } => {
for specifier in specifiers {
imports.insert(specifier.clone());
}
}
_ => {}
}
}
fn collect_used_identifiers(expr: &JsExpr, used: &mut HashSet<String>) {
match expr {
JsExpr::Identifier(name, _, _) => {
used.insert(name.clone());
}
JsExpr::Binary { left, right, .. } => {
collect_used_identifiers(left, used);
collect_used_identifiers(right, used);
}
JsExpr::Unary { argument, .. } => {
collect_used_identifiers(argument, used);
}
JsExpr::Call { callee, args, .. } => {
collect_used_identifiers(callee, used);
for arg in args {
collect_used_identifiers(arg, used);
}
}
JsExpr::Member { object, property, computed, .. } => {
collect_used_identifiers(object, used);
if *computed {
collect_used_identifiers(property, used);
}
}
JsExpr::Array(elements, _, _) => {
for el in elements {
collect_used_identifiers(el, used);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
collect_used_identifiers(value, used);
}
}
JsExpr::ArrowFunction { body, .. } => {
collect_used_identifiers(body, used);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
collect_used_identifiers(test, used);
collect_used_identifiers(consequent, used);
collect_used_identifiers(alternate, used);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
collect_used_identifiers(e, used);
}
}
_ => {}
}
}
fn collect_used_identifiers_in_stmt(stmt: &JsStmt, used: &mut HashSet<String>) {
match stmt {
JsStmt::Expr(expr, _, _) => collect_used_identifiers(expr, used),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
collect_used_identifiers(e, used);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
collect_used_identifiers(e, used);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
collect_used_identifiers(test, used);
collect_used_identifiers_in_stmt(consequent, used);
if let Some(alt) = alternate {
collect_used_identifiers_in_stmt(alt, used);
}
}
JsStmt::While { test, body, .. } => {
collect_used_identifiers(test, used);
collect_used_identifiers_in_stmt(body, used);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
collect_used_identifiers_in_stmt(i, used);
}
if let Some(t) = test {
collect_used_identifiers(t, used);
}
if let Some(u) = update {
collect_used_identifiers(u, used);
}
collect_used_identifiers_in_stmt(body, used);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
collect_used_identifiers_in_stmt(s, used);
}
}
_ => {}
}
}
for stmt in &program.body {
collect_used_identifiers_in_stmt(stmt, &mut used_imports);
}
for import in &imports {
if !used_imports.contains(import) {
report.add_issue(IssueLevel::Warning, self.code(), format!("Unused import: {}", import), None);
}
}
}
}
pub struct MemoryLeakRule;
impl Rule for MemoryLeakRule {
fn code(&self) -> String {
"memory-leak".to_string()
}
fn description(&self) -> String {
"检查可能的内存泄漏".to_string()
}
fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
let mut set_intervals = HashSet::new();
let mut set_timeouts = HashSet::new();
let mut event_listeners = HashSet::new();
fn check_memory_leaks(expr: &JsExpr, set_intervals: &mut HashSet<String>, set_timeouts: &mut HashSet<String>, event_listeners: &mut HashSet<String>, report: &mut AnalysisReport) {
match expr {
JsExpr::Call { callee, args, span, .. } => {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "setInterval" {
if let Some(id_expr) = args.get(2) {
if let JsExpr::Identifier(id, _, _) = id_expr {
set_intervals.insert(id.clone());
}
}
else {
report.add_issue(IssueLevel::Warning, "memory-leak".to_string(), "setInterval without an ID may cause memory leaks".to_string(), Some(*span));
}
}
else if name == "setTimeout" {
if let Some(id_expr) = args.get(2) {
if let JsExpr::Identifier(id, _, _) = id_expr {
set_timeouts.insert(id.clone());
}
}
}
}
if let JsExpr::Member { object, property, computed: false, .. } = &**callee {
if let JsExpr::Identifier(prop_name, _, _) = &**property {
if prop_name == "addEventListener" {
event_listeners.insert(format!("{:?}.addEventListener", object));
}
}
}
check_memory_leaks(callee, set_intervals, set_timeouts, event_listeners, report);
for arg in args {
check_memory_leaks(arg, set_intervals, set_timeouts, event_listeners, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_memory_leaks(object, set_intervals, set_timeouts, event_listeners, report);
if *computed {
check_memory_leaks(property, set_intervals, set_timeouts, event_listeners, report);
}
}
JsExpr::Binary { left, right, .. } => {
check_memory_leaks(left, set_intervals, set_timeouts, event_listeners, report);
check_memory_leaks(right, set_intervals, set_timeouts, event_listeners, report);
}
JsExpr::Unary { argument, .. } => {
check_memory_leaks(argument, set_intervals, set_timeouts, event_listeners, report);
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_memory_leaks(el, set_intervals, set_timeouts, event_listeners, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_memory_leaks(value, set_intervals, set_timeouts, event_listeners, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_memory_leaks(body, set_intervals, set_timeouts, event_listeners, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_memory_leaks(test, set_intervals, set_timeouts, event_listeners, report);
check_memory_leaks(consequent, set_intervals, set_timeouts, event_listeners, report);
check_memory_leaks(alternate, set_intervals, set_timeouts, event_listeners, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_memory_leaks(e, set_intervals, set_timeouts, event_listeners, report);
}
}
_ => {}
}
}
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) {
match stmt {
JsStmt::Expr(expr, _, _) => check_memory_leaks(expr, set_intervals, set_timeouts, event_listeners, report),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_memory_leaks(e, set_intervals, set_timeouts, event_listeners, report);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_memory_leaks(e, set_intervals, set_timeouts, event_listeners, report);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_memory_leaks(test, set_intervals, set_timeouts, event_listeners, report);
check_memory_leaks_in_stmt(consequent, set_intervals, set_timeouts, event_listeners, report);
if let Some(alt) = alternate {
check_memory_leaks_in_stmt(alt, set_intervals, set_timeouts, event_listeners, report);
}
}
JsStmt::While { test, body, .. } => {
check_memory_leaks(test, set_intervals, set_timeouts, event_listeners, report);
check_memory_leaks_in_stmt(body, set_intervals, set_timeouts, event_listeners, report);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_memory_leaks_in_stmt(i, set_intervals, set_timeouts, event_listeners, report);
}
if let Some(t) = test {
check_memory_leaks(t, set_intervals, set_timeouts, event_listeners, report);
}
if let Some(u) = update {
check_memory_leaks(u, set_intervals, set_timeouts, event_listeners, report);
}
check_memory_leaks_in_stmt(body, set_intervals, set_timeouts, event_listeners, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_memory_leaks_in_stmt(s, set_intervals, set_timeouts, event_listeners, report);
}
}
_ => {}
}
}
for stmt in &program.body {
check_memory_leaks_in_stmt(stmt, &mut set_intervals, &mut set_timeouts, &mut event_listeners, report);
}
let mut clear_intervals = HashSet::new();
let mut clear_timeouts = HashSet::new();
let mut remove_event_listeners = HashSet::new();
fn check_clear_functions(expr: &JsExpr, clear_intervals: &mut HashSet<String>, clear_timeouts: &mut HashSet<String>, remove_event_listeners: &mut HashSet<String>) {
match expr {
JsExpr::Call { callee, args, .. } => {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "clearInterval" {
if let Some(id_expr) = args.get(0) {
if let JsExpr::Identifier(id, _, _) = id_expr {
clear_intervals.insert(id.clone());
}
}
}
else if name == "clearTimeout" {
if let Some(id_expr) = args.get(0) {
if let JsExpr::Identifier(id, _, _) = id_expr {
clear_timeouts.insert(id.clone());
}
}
}
}
if let JsExpr::Member { property, computed: false, .. } = &**callee {
if let JsExpr::Identifier(prop_name, _, _) = &**property {
if prop_name == "removeEventListener" {
remove_event_listeners.insert("removeEventListener".to_string());
}
}
}
check_clear_functions(callee, clear_intervals, clear_timeouts, remove_event_listeners);
for arg in args {
check_clear_functions(arg, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_clear_functions(object, clear_intervals, clear_timeouts, remove_event_listeners);
if *computed {
check_clear_functions(property, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsExpr::Binary { left, right, .. } => {
check_clear_functions(left, clear_intervals, clear_timeouts, remove_event_listeners);
check_clear_functions(right, clear_intervals, clear_timeouts, remove_event_listeners);
}
JsExpr::Unary { argument, .. } => {
check_clear_functions(argument, clear_intervals, clear_timeouts, remove_event_listeners);
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_clear_functions(el, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_clear_functions(value, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_clear_functions(body, clear_intervals, clear_timeouts, remove_event_listeners);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_clear_functions(test, clear_intervals, clear_timeouts, remove_event_listeners);
check_clear_functions(consequent, clear_intervals, clear_timeouts, remove_event_listeners);
check_clear_functions(alternate, clear_intervals, clear_timeouts, remove_event_listeners);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_clear_functions(e, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
_ => {}
}
}
fn check_clear_functions_in_stmt(stmt: &JsStmt, clear_intervals: &mut HashSet<String>, clear_timeouts: &mut HashSet<String>, remove_event_listeners: &mut HashSet<String>) {
match stmt {
JsStmt::Expr(expr, _, _) => check_clear_functions(expr, clear_intervals, clear_timeouts, remove_event_listeners),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_clear_functions(e, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_clear_functions(e, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_clear_functions(test, clear_intervals, clear_timeouts, remove_event_listeners);
check_clear_functions_in_stmt(consequent, clear_intervals, clear_timeouts, remove_event_listeners);
if let Some(alt) = alternate {
check_clear_functions_in_stmt(alt, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
JsStmt::While { test, body, .. } => {
check_clear_functions(test, clear_intervals, clear_timeouts, remove_event_listeners);
check_clear_functions_in_stmt(body, clear_intervals, clear_timeouts, remove_event_listeners);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_clear_functions_in_stmt(i, clear_intervals, clear_timeouts, remove_event_listeners);
}
if let Some(t) = test {
check_clear_functions(t, clear_intervals, clear_timeouts, remove_event_listeners);
}
if let Some(u) = update {
check_clear_functions(u, clear_intervals, clear_timeouts, remove_event_listeners);
}
check_clear_functions_in_stmt(body, clear_intervals, clear_timeouts, remove_event_listeners);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_clear_functions_in_stmt(s, clear_intervals, clear_timeouts, remove_event_listeners);
}
}
_ => {}
}
}
for stmt in &program.body {
check_clear_functions_in_stmt(stmt, &mut clear_intervals, &mut clear_timeouts, &mut remove_event_listeners);
}
for interval_id in &set_intervals {
if !clear_intervals.contains(interval_id) {
report.add_issue(IssueLevel::Warning, self.code(), format!("setInterval with ID '{}' may not be cleared, potential memory leak", interval_id), None);
}
}
if !event_listeners.is_empty() && remove_event_listeners.is_empty() {
report.add_issue(IssueLevel::Warning, self.code(), "Event listeners added but not removed, potential memory leak".to_string(), None);
}
}
}
pub struct PerformanceBottleneckRule;
impl Rule for PerformanceBottleneckRule {
fn code(&self) -> String {
"performance-bottleneck".to_string()
}
fn description(&self) -> String {
"检查可能的性能瓶颈".to_string()
}
fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
fn check_nested_loops(stmt: &JsStmt, loop_depth: u32, report: &mut AnalysisReport) {
match stmt {
JsStmt::For { body, span, .. } => {
let new_depth = loop_depth + 1;
if new_depth >= 3 {
report.add_issue(IssueLevel::Warning, "performance-bottleneck".to_string(), "Deeply nested loops (3+ levels) may cause performance issues".to_string(), Some(*span));
}
check_nested_loops(body, new_depth, report);
}
JsStmt::While { body, span, .. } => {
let new_depth = loop_depth + 1;
if new_depth >= 3 {
report.add_issue(IssueLevel::Warning, "performance-bottleneck".to_string(), "Deeply nested loops (3+ levels) may cause performance issues".to_string(), Some(*span));
}
check_nested_loops(body, new_depth, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_nested_loops(s, loop_depth, report);
}
}
JsStmt::If { consequent, alternate, .. } => {
check_nested_loops(consequent, loop_depth, report);
if let Some(alt) = alternate {
check_nested_loops(alt, loop_depth, report);
}
}
_ => {}
}
}
fn check_dom_operations(expr: &JsExpr, report: &mut AnalysisReport) {
match expr {
JsExpr::Call { callee, args, span, .. } => {
if let JsExpr::Member { property, computed: false, .. } = &**callee {
if let JsExpr::Identifier(prop_name, _, _) = &**property {
if prop_name == "getElementById" || prop_name == "querySelector" || prop_name == "querySelectorAll" {
report.add_issue(IssueLevel::Info, "performance-bottleneck".to_string(), "Frequent DOM queries may cause performance issues, consider caching results".to_string(), Some(*span));
}
}
}
check_dom_operations(callee, report);
for arg in args {
check_dom_operations(arg, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_dom_operations(object, report);
if *computed {
check_dom_operations(property, report);
}
}
JsExpr::Binary { left, right, .. } => {
check_dom_operations(left, report);
check_dom_operations(right, report);
}
JsExpr::Unary { argument, .. } => {
check_dom_operations(argument, report);
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_dom_operations(el, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_dom_operations(value, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_dom_operations(body, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_dom_operations(test, report);
check_dom_operations(consequent, report);
check_dom_operations(alternate, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_dom_operations(e, report);
}
}
_ => {}
}
}
fn check_dom_operations_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
match stmt {
JsStmt::Expr(expr, _, _) => check_dom_operations(expr, report),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_dom_operations(e, report);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_dom_operations(e, report);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_dom_operations(test, report);
check_dom_operations_in_stmt(consequent, report);
if let Some(alt) = alternate {
check_dom_operations_in_stmt(alt, report);
}
}
JsStmt::While { test, body, .. } => {
check_dom_operations(test, report);
check_dom_operations_in_stmt(body, report);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_dom_operations_in_stmt(i, report);
}
if let Some(t) = test {
check_dom_operations(t, report);
}
if let Some(u) = update {
check_dom_operations(u, report);
}
check_dom_operations_in_stmt(body, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_dom_operations_in_stmt(s, report);
}
}
_ => {}
}
}
fn check_large_array_operations(expr: &JsExpr, report: &mut AnalysisReport) {
match expr {
JsExpr::Call { callee, args, span, .. } => {
if let JsExpr::Member { property, computed: false, .. } = &**callee {
if let JsExpr::Identifier(prop_name, _, _) = &**property {
if prop_name == "map" || prop_name == "filter" || prop_name == "reduce" || prop_name == "forEach" {
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));
}
}
}
check_large_array_operations(callee, report);
for arg in args {
check_large_array_operations(arg, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_large_array_operations(object, report);
if *computed {
check_large_array_operations(property, report);
}
}
JsExpr::Binary { left, right, .. } => {
check_large_array_operations(left, report);
check_large_array_operations(right, report);
}
JsExpr::Unary { argument, .. } => {
check_large_array_operations(argument, report);
}
JsExpr::Array(elements, span, _) => {
if elements.len() > 100 {
report.add_issue(IssueLevel::Warning, "performance-bottleneck".to_string(), "Large array literals may cause performance issues".to_string(), Some(*span));
}
for el in elements {
check_large_array_operations(el, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_large_array_operations(value, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_large_array_operations(body, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_large_array_operations(test, report);
check_large_array_operations(consequent, report);
check_large_array_operations(alternate, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_large_array_operations(e, report);
}
}
_ => {}
}
}
fn check_large_array_operations_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
match stmt {
JsStmt::Expr(expr, _, _) => check_large_array_operations(expr, report),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_large_array_operations(e, report);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_large_array_operations(e, report);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_large_array_operations(test, report);
check_large_array_operations_in_stmt(consequent, report);
if let Some(alt) = alternate {
check_large_array_operations_in_stmt(alt, report);
}
}
JsStmt::While { test, body, .. } => {
check_large_array_operations(test, report);
check_large_array_operations_in_stmt(body, report);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_large_array_operations_in_stmt(i, report);
}
if let Some(t) = test {
check_large_array_operations(t, report);
}
if let Some(u) = update {
check_large_array_operations(u, report);
}
check_large_array_operations_in_stmt(body, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_large_array_operations_in_stmt(s, report);
}
}
_ => {}
}
}
for stmt in &program.body {
check_nested_loops(stmt, 0, report);
check_dom_operations_in_stmt(stmt, report);
check_large_array_operations_in_stmt(stmt, report);
}
}
}
pub struct SecurityRule;
impl Rule for SecurityRule {
fn code(&self) -> String {
"security".to_string()
}
fn description(&self) -> String {
"检查安全问题".to_string()
}
fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
fn check_sql_injection(expr: &JsExpr, report: &mut AnalysisReport) {
match expr {
JsExpr::Binary { left, right, op, span, .. } => {
if *op == "+" {
let mut has_sql_keyword = false;
let mut has_user_input = false;
fn check_sql_keywords(expr: &JsExpr) -> bool {
match expr {
JsExpr::Literal(NargoValue::String(s), _, _) => {
let s_lower = s.to_lowercase();
s_lower.contains("select") || s_lower.contains("insert") || s_lower.contains("update") || s_lower.contains("delete") || s_lower.contains("from") || s_lower.contains("where")
}
JsExpr::Binary { left, right, .. } => check_sql_keywords(left) || check_sql_keywords(right),
JsExpr::Identifier(_, _, _) => true, _ => false,
}
}
has_sql_keyword = check_sql_keywords(left) || check_sql_keywords(right);
has_user_input = matches!(&**left, JsExpr::Identifier(_, _, _)) || matches!(&**right, JsExpr::Identifier(_, _, _));
if has_sql_keyword && has_user_input {
report.add_issue(IssueLevel::Error, "security".to_string(), "Potential SQL injection vulnerability: avoid string concatenation for SQL queries".to_string(), Some(*span));
}
}
check_sql_injection(left, report);
check_sql_injection(right, report);
}
JsExpr::Call { callee, args, .. } => {
check_sql_injection(callee, report);
for arg in args {
check_sql_injection(arg, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_sql_injection(object, report);
if *computed {
check_sql_injection(property, report);
}
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_sql_injection(el, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_sql_injection(value, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_sql_injection(body, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_sql_injection(test, report);
check_sql_injection(consequent, report);
check_sql_injection(alternate, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_sql_injection(e, report);
}
}
_ => {}
}
}
fn check_xss(expr: &JsExpr, report: &mut AnalysisReport) {
match expr {
JsExpr::Member { object, property, computed: false, span, .. } => {
if let JsExpr::Identifier(prop_name, _, _) = &**property {
if prop_name == "innerHTML" {
report.add_issue(IssueLevel::Warning, "security".to_string(), "Potential XSS vulnerability: avoid using innerHTML with untrusted data".to_string(), Some(*span));
}
}
check_xss(object, report);
}
JsExpr::Call { callee, args, span, .. } => {
if let JsExpr::Member { property, computed: false, .. } = &**callee {
if let JsExpr::Identifier(prop_name, _, _) = &**property {
if prop_name == "write" {
report.add_issue(IssueLevel::Warning, "security".to_string(), "Potential XSS vulnerability: avoid using document.write".to_string(), Some(*span));
}
}
}
check_xss(callee, report);
for arg in args {
check_xss(arg, report);
}
}
JsExpr::Binary { left, right, .. } => {
check_xss(left, report);
check_xss(right, report);
}
JsExpr::Unary { argument, .. } => {
check_xss(argument, report);
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_xss(el, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_xss(value, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_xss(body, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_xss(test, report);
check_xss(consequent, report);
check_xss(alternate, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_xss(e, report);
}
}
_ => {}
}
}
fn check_password_storage(expr: &JsExpr, report: &mut AnalysisReport) {
match expr {
JsExpr::Call { callee, args, span, .. } => {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "localStorage" || name == "sessionStorage" {
for arg in args {
if let JsExpr::Literal(NargoValue::String(s), _, _) = arg {
let s_lower = s.to_lowercase();
if s_lower.contains("password") || s_lower.contains("pwd") {
report.add_issue(IssueLevel::Error, "security".to_string(), "Insecure password storage: avoid storing passwords in localStorage/sessionStorage".to_string(), Some(*span));
}
}
}
}
}
check_password_storage(callee, report);
for arg in args {
check_password_storage(arg, report);
}
}
JsExpr::Member { object, property, computed, .. } => {
check_password_storage(object, report);
if *computed {
check_password_storage(property, report);
}
}
JsExpr::Binary { left, right, .. } => {
check_password_storage(left, report);
check_password_storage(right, report);
}
JsExpr::Unary { argument, .. } => {
check_password_storage(argument, report);
}
JsExpr::Array(elements, _, _) => {
for el in elements {
check_password_storage(el, report);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
check_password_storage(value, report);
}
}
JsExpr::ArrowFunction { body, .. } => {
check_password_storage(body, report);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
check_password_storage(test, report);
check_password_storage(consequent, report);
check_password_storage(alternate, report);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
check_password_storage(e, report);
}
}
_ => {}
}
}
fn check_security_in_stmt(stmt: &JsStmt, report: &mut AnalysisReport) {
match stmt {
JsStmt::Expr(expr, _, _) => {
check_sql_injection(expr, report);
check_xss(expr, report);
check_password_storage(expr, report);
}
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
check_sql_injection(e, report);
check_xss(e, report);
check_password_storage(e, report);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
check_sql_injection(e, report);
check_xss(e, report);
check_password_storage(e, report);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
check_sql_injection(test, report);
check_xss(test, report);
check_password_storage(test, report);
check_security_in_stmt(consequent, report);
if let Some(alt) = alternate {
check_security_in_stmt(alt, report);
}
}
JsStmt::While { test, body, .. } => {
check_sql_injection(test, report);
check_xss(test, report);
check_password_storage(test, report);
check_security_in_stmt(body, report);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
check_security_in_stmt(i, report);
}
if let Some(t) = test {
check_sql_injection(t, report);
check_xss(t, report);
check_password_storage(t, report);
}
if let Some(u) = update {
check_sql_injection(u, report);
check_xss(u, report);
check_password_storage(u, report);
}
check_security_in_stmt(body, report);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_security_in_stmt(s, report);
}
}
_ => {}
}
}
for stmt in &program.body {
check_security_in_stmt(stmt, report);
}
}
}
pub struct CodeStyleRule;
impl Rule for CodeStyleRule {
fn code(&self) -> String {
"code-style".to_string()
}
fn description(&self) -> String {
"检查代码风格问题".to_string()
}
fn check(&self, program: &JsProgram, _meta: &ScriptMetadata, report: &mut AnalysisReport) {
fn check_naming_conventions(stmt: &JsStmt, report: &mut AnalysisReport) {
match stmt {
JsStmt::VariableDecl { id, span, .. } => {
if id.starts_with('[') && id.ends_with(']') {
let content = &id[1..id.len() - 1];
for part in content.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
check_variable_name(trimmed, *span, report);
}
}
}
else {
check_variable_name(id, *span, report);
}
}
JsStmt::FunctionDecl { id, span, .. } => {
if !id.starts_with(|c: char| c.is_ascii_lowercase()) {
report.add_issue(IssueLevel::Warning, "code-style".to_string(), format!("Function name '{}' should use camelCase", id), Some(*span));
}
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
check_naming_conventions(s, report);
}
}
JsStmt::If { consequent, alternate, .. } => {
check_naming_conventions(consequent, report);
if let Some(alt) = alternate {
check_naming_conventions(alt, report);
}
}
JsStmt::While { body, .. } => {
check_naming_conventions(body, report);
}
JsStmt::For { body, .. } => {
check_naming_conventions(body, report);
}
_ => {}
}
}
fn check_variable_name(name: &str, span: Span, report: &mut AnalysisReport) {
if name.starts_with(|c: char| c.is_ascii_uppercase()) {
report.add_issue(IssueLevel::Warning, "code-style".to_string(), format!("Variable name '{}' should use camelCase", name), Some(span));
}
if name.len() == 1 && !"ijklmn".contains(name) {
report.add_issue(IssueLevel::Info, "code-style".to_string(), format!("Variable name '{}' is too short, consider using a more descriptive name", name), Some(span));
}
}
fn check_code_length(stmt: &JsStmt, report: &mut AnalysisReport) {
match stmt {
JsStmt::FunctionDecl { body, span, .. } => {
let body_length = body.len();
if body_length > 50 {
report.add_issue(IssueLevel::Warning, "code-style".to_string(), "Function is too long (over 50 statements), consider refactoring".to_string(), Some(*span));
}
}
JsStmt::Block(stmts, span, ..) => {
let block_length = stmts.len();
if block_length > 30 {
report.add_issue(IssueLevel::Info, "code-style".to_string(), "Block is too long (over 30 statements), consider refactoring".to_string(), Some(*span));
}
}
JsStmt::If { consequent, alternate, .. } => {
check_code_length(consequent, report);
if let Some(alt) = alternate {
check_code_length(alt, report);
}
}
JsStmt::While { body, .. } => {
check_code_length(body, report);
}
JsStmt::For { body, .. } => {
check_code_length(body, report);
}
_ => {}
}
}
fn count_statements(stmt: &JsStmt) -> usize {
match stmt {
JsStmt::Block(stmts, _, _) => stmts.len(),
_ => 1,
}
}
fn check_whitespace(stmt: &JsStmt, report: &mut AnalysisReport) {
}
for stmt in &program.body {
check_naming_conventions(stmt, report);
check_code_length(stmt, report);
check_whitespace(stmt, report);
}
}
}
#[derive(Default)]
pub struct ScriptAnalyzer;
impl ScriptAnalyzer {
pub fn new() -> Self {
Self
}
pub fn analyze(&self, program: &JsProgram) -> Result<ScriptMetadata> {
let mut meta = ScriptMetadata::default();
let mut declared_vars = HashSet::new();
let mut declared_functions = HashSet::new();
for stmt in &program.body {
match stmt {
JsStmt::VariableDecl { id, init, .. } => {
self.analyze_variable_decl(id, init.as_ref(), &mut meta);
if id.starts_with('[') && id.ends_with(']') {
let content = &id[1..id.len() - 1];
for part in content.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
declared_vars.insert(trimmed.to_string());
}
}
}
else {
declared_vars.insert(id.to_string());
}
}
JsStmt::FunctionDecl { id, params: _, body: _, .. } => {
meta.actions.insert(id.clone());
declared_functions.insert(id.clone());
}
JsStmt::Expr(expr, _, _) => {
self.analyze_expression(expr, &mut meta);
}
_ => {}
}
}
for stmt in &program.body {
match stmt {
JsStmt::VariableDecl { id, init, .. } => {
if let Some(init_expr) = init {
let mut deps = HashSet::new();
self.find_dependencies(init_expr, &mut deps, &meta);
if !deps.is_empty() {
meta.dependencies.insert(id.clone(), deps);
}
}
}
JsStmt::FunctionDecl { id, body, .. } => {
let mut deps = HashSet::new();
for s in body {
self.find_dependencies_in_stmt(s, &mut deps, &meta);
}
if !deps.is_empty() {
meta.dependencies.insert(id.clone(), deps);
}
}
JsStmt::Expr(expr, _, _) => {
if let JsExpr::Call { callee, args, .. } = expr {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "$effect" || name == "watchEffect" {
if let Some(first_arg) = args.get(0) {
let mut deps = HashSet::new();
self.find_dependencies(first_arg, &mut deps, &meta);
if !deps.is_empty() {
meta.dependencies.insert(format!("$effect_{}", meta.dependencies.len()), deps);
}
}
}
}
}
}
_ => {}
}
}
Ok(meta)
}
pub fn analyze_with_rules(&self, program: &JsProgram, file_path: Option<String>) -> Result<(ScriptMetadata, AnalysisReport)> {
let start_time = std::time::Instant::now();
let meta = self.analyze(program)?;
let mut report = AnalysisReport::new(file_path);
let rule_engine = default_rule_engine();
rule_engine.run(program, &meta, &mut report);
let duration = start_time.elapsed();
report.duration_ms = duration.as_millis() as u64;
report.sort_issues();
Ok((meta, report))
}
fn analyze_variable_decl(&self, id: &str, init: Option<&JsExpr>, meta: &mut ScriptMetadata) {
let mut ids = Vec::new();
if id.starts_with('[') && id.ends_with(']') {
let content = &id[1..id.len() - 1];
for part in content.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
ids.push(trimmed.to_string());
}
}
}
else {
ids.push(id.to_string());
}
if id == "props" {
if let Some(JsExpr::Call { callee, args, .. }) = init {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "defineProps" {
self.extract_keys_from_args(args, &mut meta.props);
return;
}
}
}
}
if id == "emit" || id == "emits" {
if let Some(JsExpr::Call { callee, args, .. }) = init {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "defineEmits" {
self.extract_keys_from_args(args, &mut meta.emits);
return;
}
}
}
}
if let Some(init_expr) = init {
match init_expr {
JsExpr::Call { callee, .. } => {
if let JsExpr::Identifier(name, _, _) = &**callee {
if name == "signal" || name == "createSignal" || name == "ref" || name == "reactive" {
for (i, var_id) in ids.iter().enumerate() {
if i == 0 || name == "ref" || name == "reactive" {
meta.signals.insert(var_id.clone());
}
}
}
else if name == "computed" || name == "$computed" || name == "createComputed" {
for var_id in &ids {
meta.computed.insert(var_id.clone());
}
}
}
}
JsExpr::ArrowFunction { .. } => {
for var_id in &ids {
meta.actions.insert(var_id.clone());
}
}
_ => {}
}
}
}
fn analyze_expression(&self, expr: &JsExpr, meta: &mut ScriptMetadata) {
if let JsExpr::Call { callee, args, .. } = expr {
if let JsExpr::Identifier(name, _, _) = &**callee {
match name.as_str() {
"defineProps" => {
self.extract_keys_from_args(args, &mut meta.props);
}
"defineEmits" => {
self.extract_keys_from_args(args, &mut meta.emits);
}
_ => {}
}
}
}
}
fn find_dependencies(&self, expr: &JsExpr, deps: &mut HashSet<String>, meta: &ScriptMetadata) {
match expr {
JsExpr::Identifier(name, _, _) => {
if meta.signals.contains(name) || meta.computed.contains(name) || meta.props.contains(name) {
deps.insert(name.clone());
}
}
JsExpr::Binary { left, right, .. } => {
self.find_dependencies(left, deps, meta);
self.find_dependencies(right, deps, meta);
}
JsExpr::Unary { argument, .. } => {
self.find_dependencies(argument, deps, meta);
}
JsExpr::Call { callee, args, .. } => {
self.find_dependencies(callee, deps, meta);
for arg in args {
self.find_dependencies(arg, deps, meta);
}
}
JsExpr::Member { object, property, computed, .. } => {
self.find_dependencies(object, deps, meta);
if *computed {
self.find_dependencies(property, deps, meta);
}
}
JsExpr::Array(elements, _, _) => {
for el in elements {
self.find_dependencies(el, deps, meta);
}
}
JsExpr::Object(properties, _, _) => {
for value in properties.values() {
self.find_dependencies(value, deps, meta);
}
}
JsExpr::ArrowFunction { body, .. } => {
self.find_dependencies(body, deps, meta);
}
JsExpr::Conditional { test, consequent, alternate, .. } => {
self.find_dependencies(test, deps, meta);
self.find_dependencies(consequent, deps, meta);
self.find_dependencies(alternate, deps, meta);
}
JsExpr::TemplateLiteral { expressions, .. } => {
for e in expressions {
self.find_dependencies(e, deps, meta);
}
}
_ => {}
}
}
fn find_dependencies_in_stmt(&self, stmt: &JsStmt, deps: &mut HashSet<String>, meta: &ScriptMetadata) {
match stmt {
JsStmt::Expr(expr, _, _) => self.find_dependencies(expr, deps, meta),
JsStmt::VariableDecl { init, .. } => {
if let Some(e) = init {
self.find_dependencies(e, deps, meta);
}
}
JsStmt::Return(expr, _, _) => {
if let Some(e) = expr {
self.find_dependencies(e, deps, meta);
}
}
JsStmt::If { test, consequent, alternate, .. } => {
self.find_dependencies(test, deps, meta);
self.find_dependencies_in_stmt(consequent, deps, meta);
if let Some(alt) = alternate {
self.find_dependencies_in_stmt(alt, deps, meta);
}
}
JsStmt::While { test, body, .. } => {
self.find_dependencies(test, deps, meta);
self.find_dependencies_in_stmt(body, deps, meta);
}
JsStmt::For { init, test, update, body, .. } => {
if let Some(i) = init {
self.find_dependencies_in_stmt(i, deps, meta);
}
if let Some(t) = test {
self.find_dependencies(t, deps, meta);
}
if let Some(u) = update {
self.find_dependencies(u, deps, meta);
}
self.find_dependencies_in_stmt(body, deps, meta);
}
JsStmt::Block(stmts, _, _) => {
for s in stmts {
self.find_dependencies_in_stmt(s, deps, meta);
}
}
_ => {}
}
}
fn extract_keys_from_args(&self, args: &[JsExpr], set: &mut HashSet<String>) {
if let Some(first_arg) = args.get(0) {
match first_arg {
JsExpr::Object(map, _, _) => {
for key in map.keys() {
set.insert(key.clone());
}
}
JsExpr::Array(arr, _, _) => {
for item in arr {
if let JsExpr::Literal(NargoValue::String(s), _, _) = item {
set.insert(s.clone());
}
}
}
_ => {}
}
}
}
}
pub fn default_rule_engine() -> RuleEngine {
let mut engine = RuleEngine::new();
engine.add_rule(Box::new(UnusedVariableRule));
engine.add_rule(Box::new(UndefinedVariableRule));
engine.add_rule(Box::new(UnsafeOperationRule));
engine.add_rule(Box::new(UnusedImportRule));
engine.add_rule(Box::new(MemoryLeakRule));
engine.add_rule(Box::new(PerformanceBottleneckRule));
engine.add_rule(Box::new(SecurityRule));
engine.add_rule(Box::new(CodeStyleRule));
engine
}