rslint_core/
lib.rs

1//! The core runner for RSLint responsible for the bulk of the linter's work.
2//!
3//! The crate is not RSLint-specific and can be used from any project. The runner is responsible
4//! for taking a list of rules, and source code and running the linter on it. It is important to decouple
5//! the CLI work and the low level linting work from eachother to be able to reuse the linter facilities.
6//! Therefore, the core runner should never do anything `rslint_cli`-specific.
7//!
8//! The structure at the core of the crate is the [`CstRule`] and [`Rule`] traits.
9//! CST rules run on a single file and its concrete syntax tree produced by [`rslint_parser`].
10//! The rules have a couple of restrictions for clarity and speed, these include:
11//! - all cst rules must be [`Send`](std::marker::Send) and [`Sync`](std::marker::Sync) so they can be run in parallel
12//! - rules may never rely on the results of other rules, this is impossible because rules are run in parallel
13//! - rules should never make any network or file requests
14//!
15//! ## Using the runner
16//!
17//! To run the runner you must first create a [`CstRuleStore`], which is the structure used for storing what rules
18//! to run. Then you can use [`lint_file`].
19//!
20//! ## Running a single rule
21//!
22//! To run a single rule you can find the rule you want in the `groups` module and submodules within. Then
23//! to run a rule in full on a syntax tree you can use [`run_rule`].
24//!
25//! Rules can also be run on individual nodes using the functions on [`CstRule`].
26//! ⚠️ note however that many rules rely on checking tokens or the root and running on single nodes
27//! may yield incorrect results, you should only do this if you know about the rule's implementation.
28
29// FIXME: Workaround for https://github.com/GREsau/schemars/pull/65
30#![allow(clippy::field_reassign_with_default)]
31
32mod file;
33mod rule;
34mod store;
35mod testing;
36
37pub mod autofix;
38pub mod directives;
39pub mod groups;
40pub mod rule_prelude;
41pub mod util;
42
43pub use self::{
44    file::File,
45    rule::{CstRule, Inferable, Outcome, Rule, RuleCtx, RuleLevel, RuleResult, Tag},
46    store::CstRuleStore,
47};
48pub use rslint_errors::{Diagnostic, Severity, Span};
49
50pub use crate::directives::{
51    apply_top_level_directives, skip_node, Directive, DirectiveError, DirectiveErrorKind,
52    DirectiveParser,
53};
54
55use dyn_clone::clone_box;
56use rslint_parser::{util::SyntaxNodeExt, SyntaxKind, SyntaxNode};
57use std::collections::HashMap;
58use std::sync::Arc;
59
60/// The result of linting a file.
61// TODO: A lot of this stuff can be shoved behind a "linter options" struct
62#[derive(Debug, Clone)]
63pub struct LintResult<'s> {
64    /// Any diagnostics (errors, warnings, etc) emitted from the parser
65    pub parser_diagnostics: Vec<Diagnostic>,
66    /// The diagnostics emitted by each rule run
67    pub rule_results: HashMap<&'static str, RuleResult>,
68    /// Any warnings or errors emitted by the directive parser
69    pub directive_diagnostics: Vec<DirectiveError>,
70    pub store: &'s CstRuleStore,
71    pub parsed: SyntaxNode,
72    pub file_id: usize,
73    pub verbose: bool,
74    pub fixed_code: Option<String>,
75}
76
77impl LintResult<'_> {
78    /// Get all of the diagnostics thrown during linting, in the order of parser diagnostics, then
79    /// the diagnostics of each rule sequentially.
80    pub fn diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
81        self.parser_diagnostics
82            .iter()
83            .chain(
84                self.rule_results
85                    .values()
86                    .map(|x| x.diagnostics.iter())
87                    .flatten(),
88            )
89            .chain(self.directive_diagnostics.iter().map(|x| &x.diagnostic))
90    }
91
92    /// The overall outcome of linting this file (failure, warning, success, etc)
93    pub fn outcome(&self) -> Outcome {
94        self.diagnostics().into()
95    }
96
97    /// Attempt to automatically fix any fixable issues and return the fixed code.
98    ///
99    /// This will not run if there are syntax errors unless `dirty` is set to true.
100    pub fn fix(&mut self, dirty: bool, file: &File) -> Option<String> {
101        if self
102            .parser_diagnostics
103            .iter()
104            .any(|x| x.severity == Severity::Error)
105            && !dirty
106        {
107            None
108        } else {
109            Some(autofix::recursively_apply_fixes(self, file))
110        }
111    }
112}
113
114/// Lint a file with a specific rule store.
115pub fn lint_file<'s>(file: &File, store: &'s CstRuleStore, verbose: bool) -> LintResult<'s> {
116    let (diagnostics, node) = file.parse_with_errors();
117    lint_file_inner(node, diagnostics, file, store, verbose)
118}
119
120/// used by lint_file and incrementally_relint to not duplicate code
121pub(crate) fn lint_file_inner<'s>(
122    node: SyntaxNode,
123    parser_diagnostics: Vec<Diagnostic>,
124    file: &File,
125    store: &'s CstRuleStore,
126    verbose: bool,
127) -> LintResult<'s> {
128    let mut new_store = store.clone();
129    let directives::DirectiveResult {
130        directives,
131        diagnostics: mut directive_diagnostics,
132    } = { DirectiveParser::new_with_store(node.clone(), file, store).get_file_directives() };
133
134    apply_top_level_directives(
135        directives.as_slice(),
136        &mut new_store,
137        &mut directive_diagnostics,
138        file.id,
139    );
140
141    let src: Arc<str> = Arc::from(node.to_string());
142
143    // FIXME: Replace with thread pool
144    let results = new_store
145        .rules
146        .into_iter()
147        .map(|rule| {
148            (
149                rule.name(),
150                run_rule(
151                    &*rule,
152                    file.id,
153                    node.clone(),
154                    verbose,
155                    &directives,
156                    src.clone(),
157                ),
158            )
159        })
160        .collect();
161
162    LintResult {
163        parser_diagnostics,
164        rule_results: results,
165        directive_diagnostics,
166        store,
167        parsed: node,
168        file_id: file.id,
169        verbose,
170        fixed_code: None,
171    }
172}
173
174/// Run a single run on an entire parsed file.
175///
176/// # Panics
177/// Panics if `root`'s kind is not `SCRIPT` or `MODULE`
178pub fn run_rule(
179    rule: &dyn CstRule,
180    file_id: usize,
181    root: SyntaxNode,
182    verbose: bool,
183    directives: &[Directive],
184    src: Arc<str>,
185) -> RuleResult {
186    assert!(root.kind() == SyntaxKind::SCRIPT || root.kind() == SyntaxKind::MODULE);
187    let mut ctx = RuleCtx {
188        file_id,
189        verbose,
190        diagnostics: vec![],
191        fixer: None,
192        src,
193    };
194
195    rule.check_root(&root, &mut ctx);
196
197    root.descendants_with_tokens_with(&mut |elem| {
198        match elem {
199            rslint_parser::NodeOrToken::Node(node) => {
200                if skip_node(directives, node, rule) || node.kind() == SyntaxKind::ERROR {
201                    return false;
202                }
203                rule.check_node(node, &mut ctx);
204            }
205            rslint_parser::NodeOrToken::Token(tok) => {
206                let _ = rule.check_token(tok, &mut ctx);
207            }
208        };
209        true
210    });
211    RuleResult::new(ctx.diagnostics, ctx.fixer)
212}
213
214/// Get a rule by its kebab-case name.
215pub fn get_rule_by_name(name: &str) -> Option<Box<dyn CstRule>> {
216    CstRuleStore::new()
217        .builtins()
218        .rules
219        .iter()
220        .find(|rule| rule.name() == name)
221        .map(|rule| clone_box(&**rule))
222}
223
224/// Get a group's rules by the group name.
225// TODO: there should be a good way to not have to hardcode all of this
226pub fn get_group_rules_by_name(group_name: &str) -> Option<Vec<Box<dyn CstRule>>> {
227    use groups::*;
228
229    Some(match group_name {
230        "errors" => errors(),
231        "style" => style(),
232        "regex" => regex(),
233        _ => return None,
234    })
235}
236
237/// Get a suggestion for an incorrect rule name for things such as "did you mean ...?"
238pub fn get_rule_suggestion(incorrect_rule_name: &str) -> Option<&str> {
239    let rules = CstRuleStore::new()
240        .builtins()
241        .rules
242        .into_iter()
243        .map(|rule| rule.name());
244    util::find_best_match_for_name(rules, incorrect_rule_name, None)
245}
246
247/// Get a rule and its documentation.
248///
249/// This will always be `Some` for valid rule names and it will be an empty string
250/// if the rule has no docs
251pub fn get_rule_docs(rule: &str) -> Option<&'static str> {
252    get_rule_by_name(rule).map(|rule| rule.docs())
253}
254
255macro_rules! trait_obj_helper {
256    ($($name:ident),* $(,)?) => {
257        vec![
258            $(
259                Box::new($name::default()) as Box<dyn Inferable>
260            ),*
261        ]
262    }
263}
264
265/// Get all of the built in rules which can have their options inferred using multiple syntax nodes
266/// see [`Inferable`] for more information.
267pub fn get_inferable_rules() -> Vec<Box<dyn Inferable>> {
268    use groups::style::*;
269
270    trait_obj_helper![BlockSpacing]
271}