Skip to main content

normalize_syntax_rules/
lib.rs

1//! Syntax-based linting with tree-sitter queries.
2//!
3//! This crate provides:
4//! - Rule loading from multiple sources (builtins, user global, project)
5//! - Rule execution with combined query optimization
6//! - Pluggable data sources for rule conditionals
7//!
8//! # Rule File Format
9//!
10//! ```scm
11//! # ---
12//! # id = "no-unwrap"
13//! # severity = "warning"
14//! # message = "Avoid unwrap() on user input"
15//! # allow = ["**/tests/**"]
16//! # requires = { "rust.edition" = ">=2024" }
17//! # enabled = true  # set to false to disable a builtin
18//! # fix = ""  # empty = delete match, or use "$capture" to substitute
19//! # ---
20//!
21//! (call_expression
22//!   function: (field_expression
23//!     field: (field_identifier) @method)
24//!   (#eq? @method "unwrap")) @match
25//! ```
26
27mod builtin;
28mod loader;
29mod runner;
30mod sources;
31
32pub use builtin::BUILTIN_RULES;
33pub use loader::{RuleOverride, RulesConfig, load_all_rules, parse_rule_content};
34pub use runner::{DebugFlags, Finding, apply_fixes, evaluate_predicates, run_rules};
35pub use sources::{
36    EnvSource, GitSource, GoSource, PathSource, PythonSource, RuleSource, RustSource,
37    SourceContext, SourceRegistry, TypeScriptSource, builtin_registry,
38};
39
40use glob::Pattern;
41use std::collections::HashMap;
42use std::path::PathBuf;
43
44/// Severity level for rule findings.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum Severity {
47    Error,
48    #[default]
49    Warning,
50    Info,
51}
52
53impl std::fmt::Display for Severity {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Severity::Error => write!(f, "error"),
57            Severity::Warning => write!(f, "warning"),
58            Severity::Info => write!(f, "info"),
59        }
60    }
61}
62
63impl std::str::FromStr for Severity {
64    type Err = String;
65
66    fn from_str(s: &str) -> Result<Self, Self::Err> {
67        match s.to_lowercase().as_str() {
68            "error" => Ok(Severity::Error),
69            "warning" | "warn" => Ok(Severity::Warning),
70            "info" | "note" => Ok(Severity::Info),
71            _ => Err(format!("unknown severity: {}", s)),
72        }
73    }
74}
75
76/// A syntax rule definition.
77#[derive(Debug)]
78pub struct Rule {
79    /// Unique identifier for this rule.
80    pub id: String,
81    /// The tree-sitter query pattern.
82    pub query_str: String,
83    /// Severity level.
84    pub severity: Severity,
85    /// Message to display when the rule matches.
86    pub message: String,
87    /// Glob patterns for files where matches are allowed.
88    pub allow: Vec<Pattern>,
89    /// Source file path of this rule (empty for builtins).
90    pub source_path: PathBuf,
91    /// Languages this rule applies to (inferred from query or explicit).
92    pub languages: Vec<String>,
93    /// Whether this rule is enabled.
94    pub enabled: bool,
95    /// Whether this is a builtin rule.
96    pub builtin: bool,
97    /// Conditions that must be met for this rule to apply.
98    /// Format: { "namespace.key" = "value" } or { "namespace.key" = ">=value" }
99    pub requires: HashMap<String, String>,
100    /// Auto-fix template using capture names from the query.
101    /// Use `$capture_name` to reference captures, `$match` for the full match.
102    /// Empty string means "delete the match".
103    pub fix: Option<String>,
104}
105
106/// A builtin rule definition (id, content).
107pub struct BuiltinRule {
108    pub id: &'static str,
109    pub content: &'static str,
110}