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}