Skip to main content

declint_core/
lib.rs

1//! Config and linting engine for [`declint`] — no LSP dependencies.
2//!
3//! An declint configuration is a YAML file of regex rules:
4//!
5//! ```yaml
6//! version: 1
7//! rules:
8//!   - id: no-tabs
9//!     pattern: '\t+'
10//!     message: "Use spaces, found '{match}'"
11//!     severity: warning
12//! ```
13//!
14//! Load it, lint text, get violations:
15//!
16//! ```
17//! use declint_core::{Config, Linter};
18//!
19//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! use declint_core::{Callbacks, Config, Linter};
21//!
22//! let config = Config::from_str(
23//!     "version: 1\nrules:\n  - id: no-tabs\n    pattern: '\\t+'\n    message: \"Use \
24//!      spaces\"\n    severity: warning\n",
25//! )?;
26//! let linter = Linter::new(config, &Callbacks::new())?;
27//!
28//! let violations = linter.lint("a\tb");
29//! assert_eq!(violations.len(), 1);
30//! assert_eq!(violations[0].rule_id, "no-tabs");
31//! assert_eq!(violations[0].span.to_range(), 1..2);
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! Everything is validated at load time — regex syntax, placeholder names,
37//! duplicate rule ids, unknown severities — so a rule file is either fully
38//! usable or rejected with the rule id and file line of the problem.
39//!
40//! Rules can also be **scoped**: a `scopes` entry segments the file (a
41//! `start` pattern begins a region, an optional `end` pattern closes it)
42//! and its `rules` run only inside those regions — the lint equivalent of
43//! a pass that expands, then subpasses that run per region:
44//!
45//! ```yaml
46//! version: 1
47//! scopes:
48//!   - id: shell-fence
49//!     start: '^```sh$'
50//!     end: '^```$'
51//!     rules:
52//!       - id: no-sudo
53//!         pattern: '\bsudo\b'
54//!         message: "Don't use sudo in scripts"
55//!         severity: error
56//! ```
57//!
58//! [`declint`]: https://crates.io/crates/declint
59
60#![forbid(unsafe_code)]
61#![deny(missing_docs)]
62
63mod callback;
64mod config;
65/// The global ruleset store: packages installed with
66/// `declint install -g`, resolved by `import: global:<pkg>`.
67pub mod store;
68
69/// The embedded preset library: curated config fragments importable as
70/// `import: preset:<name>`.
71pub mod presets;
72mod config_set;
73mod lang;
74mod linter;
75mod scopes;
76mod suppressions;
77mod template;
78
79pub use callback::{
80    Callbacks, CallbackRef, Decision, MatchCallback, MatchContext, MatchParser, RawMatch,
81};
82pub use config::{Config, ConfigError, Rule, RuleTest, Scope, SUPPORTED_VERSION};
83pub use config_set::{ConfigSet, NamedConfig, ScopeEntry, CONFIG_DIR, CONFIG_FILE, LEGACY_CONFIG_FILE};
84pub use lang::language_from_extension;
85pub use linter::{line_col, DocInfo, Linter, Span, Violation};
86pub use scopes::{segment, segment_all};
87pub use suppressions::Suppressions;
88pub use template::Template;
89
90/// How serious a violation is.
91///
92/// Rendered one-to-one as an LSP diagnostic severity by `declint-lsp`, and
93/// printed verbatim by `declint check`.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum Severity {
96    /// A definite problem.
97    Error,
98    /// A probable problem — the default for rules that omit `severity`.
99    Warning,
100    /// A suggestion.
101    Info,
102    /// A nitpick, rendered faintly by most editors.
103    Hint,
104}
105
106impl Severity {
107    /// Parses a severity from its config-file spelling.
108    pub fn parse(s: &str) -> Option<Self> {
109        match s {
110            "error" => Some(Self::Error),
111            "warning" => Some(Self::Warning),
112            "info" => Some(Self::Info),
113            "hint" => Some(Self::Hint),
114            _ => None,
115        }
116    }
117
118    /// Numeric rank for threshold comparisons: error > warning > info >
119    /// hint. (Derived ordering would rank them the other way round.)
120    pub fn rank(self) -> u8 {
121        match self {
122            Self::Error => 3,
123            Self::Warning => 2,
124            Self::Info => 1,
125            Self::Hint => 0,
126        }
127    }
128
129    /// The config-file spelling of this severity.
130    pub fn as_str(&self) -> &'static str {
131        match self {
132            Self::Error => "error",
133            Self::Warning => "warning",
134            Self::Info => "info",
135            Self::Hint => "hint",
136        }
137    }
138}
139
140impl std::fmt::Display for Severity {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.write_str(self.as_str())
143    }
144}