Skip to main content

nargo_linter/
lib.rs

1//! Nargo linter module.
2
3#![warn(missing_docs)]
4
5use async_trait::async_trait;
6use nargo_ir::IRModule;
7use nargo_types::{Error, Result};
8use serde::{Deserialize, Serialize};
9
10pub mod rules;
11
12/// Diagnostic severity.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum Severity {
15    /// Error level.
16    Error,
17    /// Warning level.
18    Warning,
19    /// Hint level.
20    Hint,
21}
22
23/// Diagnostic information.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Diagnostic {
26    /// Diagnostic code.
27    pub code: String,
28    /// Diagnostic message.
29    pub message: String,
30    /// Diagnostic severity.
31    pub severity: Severity,
32    /// Line number.
33    pub line: usize,
34    /// Column number.
35    pub column: usize,
36}
37
38/// Fix action for lint errors.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct FixAction {
41    /// Start position of the fix.
42    pub start: usize,
43    /// End position of the fix.
44    pub end: usize,
45    /// Replacement text.
46    pub replacement: String,
47    /// Description of the fix.
48    pub description: String,
49}
50
51/// Lint rule trait.
52#[async_trait]
53pub trait LintRule: Send + Sync {
54    /// Returns the rule name.
55    fn name(&self) -> &'static str;
56    /// Checks the source code.
57    async fn check(&self, source: &str, module: &IRModule) -> Result<Vec<Diagnostic>>;
58    /// Fixes the lint errors.
59    async fn fix(&self, source: &str, module: &IRModule) -> Result<Vec<FixAction>>;
60}
61
62/// Rule configuration.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct RuleConfig {
65    /// Whether the rule is enabled.
66    pub enabled: bool,
67    /// Rule severity.
68    pub severity: Option<Severity>,
69    /// Whether auto-fix is enabled for this rule.
70    pub fix: Option<bool>,
71}
72
73/// Linter configuration.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct LinterConfig {
76    /// Rules configuration.
77    pub rules: std::collections::HashMap<String, RuleConfig>,
78    /// Selected rule set.
79    pub extends: Option<String>,
80}
81
82/// Get predefined rule set.
83pub fn get_predefined_rule_set(name: &str) -> Option<LinterConfig> {
84    match name {
85        "recommended" => Some(LinterConfig { rules: vec![("no-console".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Warning), fix: None }), ("no-debugger".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-unused-vars".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Warning), fix: None }), ("no-unreachable-code".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Warning), fix: None }), ("no-magic-numbers".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Warning), fix: None }), ("no-alert".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Warning), fix: None }), ("no-undeclared-variables".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None })].into_iter().collect(), extends: None }),
86        "strict" => Some(LinterConfig {
87            rules: vec![("no-console".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-debugger".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-unused-vars".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-unreachable-code".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-empty-template".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-deprecated-tags".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-magic-numbers".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-alert".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None }), ("no-undeclared-variables".to_string(), RuleConfig { enabled: true, severity: Some(Severity::Error), fix: None })].into_iter().collect(),
88            extends: None,
89        }),
90        _ => None,
91    }
92}
93
94/// Nargo linter.
95pub struct NargoLinter {
96    rules: Vec<Box<dyn LintRule>>,
97    config: LinterConfig,
98}
99
100impl NargoLinter {
101    /// Create a new linter with default configuration.
102    pub fn new() -> Self {
103        let config = LinterConfig { rules: std::collections::HashMap::new(), extends: None };
104        Self { rules: Vec::new(), config }
105    }
106
107    /// Create a new linter with custom configuration.
108    pub fn with_config(mut config: LinterConfig) -> Self {
109        // Handle rule set extension
110        if let Some(extends) = &config.extends {
111            if let Some(rule_set) = get_predefined_rule_set(extends) {
112                // Merge rule set into config
113                for (rule_name, rule_config) in rule_set.rules {
114                    if !config.rules.contains_key(&rule_name) {
115                        config.rules.insert(rule_name, rule_config);
116                    }
117                }
118            }
119        }
120        Self { rules: Vec::new(), config }
121    }
122
123    /// Create a new linter with all built-in rules.
124    pub fn with_all_rules(config: LinterConfig) -> Self {
125        let mut linter = Self::with_config(config);
126        linter.add_builtin_rules();
127        linter
128    }
129
130    /// Add all built-in rules.
131    pub fn add_builtin_rules(&mut self) {
132        self.add_rule(Box::new(rules::NoConsole));
133        self.add_rule(Box::new(rules::NoDebugger));
134        self.add_rule(Box::new(rules::NoDeprecatedTags));
135        self.add_rule(Box::new(rules::NoEmptyTemplate));
136        self.add_rule(Box::new(rules::NoUnusedVars));
137        self.add_rule(Box::new(rules::NoUnreachableCode));
138        self.add_rule(Box::new(rules::NoMagicNumbers));
139        self.add_rule(Box::new(rules::NoAlert));
140        self.add_rule(Box::new(rules::NoUndeclaredVariables));
141    }
142
143    /// Add a lint rule.
144    pub fn add_rule(&mut self, rule: Box<dyn LintRule>) {
145        self.rules.push(rule);
146    }
147
148    /// Run all lint rules.
149    pub async fn run(&self, name: &str, source: &str) -> Result<Vec<Diagnostic>> {
150        // Create a new parser for each run
151        let registry = std::sync::Arc::new(nargo_parser::ParserRegistry::new());
152        let mut parser = nargo_parser::Parser::new(name.to_string(), source, registry);
153        let module = parser.parse_all()?;
154        let mut all_diagnostics = Vec::new();
155
156        println!("Rules count: {}", self.rules.len());
157        println!("Module name: {}", module.name);
158        println!("Module has script: {:?}", module.script.is_some());
159        println!("Module has template: {:?}", module.template.is_some());
160        if let Some(script) = &module.script {
161            println!("Script body length: {}", script.body.len());
162        }
163        if let Some(template) = &module.template {
164            println!("Template nodes length: {}", template.nodes.len());
165        }
166
167        for rule in &self.rules {
168            let rule_name = rule.name();
169            println!("Running rule: {}", rule_name);
170
171            // Check if the rule is enabled in config
172            let is_enabled = if let Some(rule_config) = self.config.rules.get(rule_name) {
173                println!("Rule {} is enabled: {}", rule_name, rule_config.enabled);
174                rule_config.enabled
175            }
176            else {
177                // Default to enabled if not in config
178                println!("Rule {} is not in config, defaulting to enabled", rule_name);
179                true
180            };
181            if is_enabled {
182                let diagnostics = rule.check(source, &module).await?;
183                println!("Rule {} found {} diagnostics", rule_name, diagnostics.len());
184                all_diagnostics.extend(diagnostics);
185            }
186        }
187
188        println!("Total diagnostics: {}", all_diagnostics.len());
189        Ok(all_diagnostics)
190    }
191
192    /// Get the current configuration.
193    pub fn config(&self) -> &LinterConfig {
194        &self.config
195    }
196
197    /// Run all lint rules and generate fix actions.
198    pub async fn run_fix(&self, name: &str, source: &str) -> Result<Vec<FixAction>> {
199        // Create a new parser for each run
200        let registry = std::sync::Arc::new(nargo_parser::ParserRegistry::new());
201        let mut parser = nargo_parser::Parser::new(name.to_string(), source, registry);
202        let module = parser.parse_all()?;
203        let mut all_fix_actions = Vec::new();
204
205        for rule in &self.rules {
206            let rule_name = rule.name();
207
208            // Check if the rule is enabled in config
209            let is_enabled = if let Some(rule_config) = self.config.rules.get(rule_name) {
210                rule_config.enabled
211            }
212            else {
213                // Default to enabled if not in config
214                true
215            };
216
217            // Check if fix is enabled for this rule
218            let is_fix_enabled = if let Some(rule_config) = self.config.rules.get(rule_name) {
219                rule_config.fix.unwrap_or(true)
220            }
221            else {
222                // Default to true if not specified
223                true
224            };
225
226            if is_enabled && is_fix_enabled {
227                let fix_actions = rule.fix(source, &module).await?;
228                all_fix_actions.extend(fix_actions);
229            }
230        }
231
232        Ok(all_fix_actions)
233    }
234
235    /// Apply fix actions to the source code.
236    pub fn apply_fixes(&self, source: &str, fix_actions: &[FixAction]) -> String {
237        // Sort fix actions by start position in reverse order to avoid position shifting
238        let mut sorted_actions = fix_actions.to_vec();
239        sorted_actions.sort_by(|a, b| b.start.cmp(&a.start));
240
241        let mut result = source.to_string();
242        for action in sorted_actions {
243            let start = action.start;
244            let end = action.end;
245            if start <= end && end <= result.len() {
246                result = format!("{}{}{}", &result[..start], action.replacement, &result[end..]);
247            }
248        }
249
250        result
251    }
252}