rslint_core/directives/
mod.rs

1//! Directives used to configure or ignore rules.
2//! These take place of comments over nodes or comments at the top level.
3//!
4//! Directives can contain multiple commands separated by `-`. For example:
5//!
6//! ```text
7//! // rslint-ignore for-direction, no-await-in-loop - deny no-empty -- because why not
8//!   |      |                                     |  |            |    |             |
9//!   |      +-------------------------------------+  +------------+    +-------------+
10//!   |                      command                     command            comment   |
11//!   +-------------------------------------------------------------------------------+
12//!                                      Directive
13//! ```
14
15pub(self) mod lexer;
16
17mod commands;
18mod parser;
19
20pub use self::commands::*;
21pub use self::parser::*;
22
23use crate::{rule_tests, CstRule, CstRuleStore, Diagnostic, SyntaxNode};
24use rslint_lexer::SyntaxKind;
25use rslint_parser::{util::*, SmolStr, TextRange, TextSize};
26
27// TODO: More complex warnings, things like ignoring node directives because of file level directives
28
29#[derive(Debug, Clone)]
30pub enum ComponentKind {
31    /// This component is a rule name (e.g. `no-extra-boolean-cast` or `no-empty-block`)
32    ///
33    /// The directive parser will not verify if the rule name is valid. This has to be done
34    /// separately.
35    Rule(Box<dyn CstRule>),
36    /// This component is the name of a directive command (e.g. `ignore`)
37    CommandName(SmolStr),
38    /// A number that is parsed by the [`Number`] instruction.
39    ///
40    /// [`Number`]: ./enum.Instruction.html
41    Number(u64),
42    /// Any literal that was parsed by the [`Literal`] instruction.
43    ///
44    /// [`Literal`]: ./enum.Instruction.html
45    Literal(&'static str),
46    /// A sequence list of parsed `ComponentKind`s.
47    Repetition(Vec<Component>),
48}
49
50impl ComponentKind {
51    /// Returns the documentation that should be shown for this document.
52    pub fn documentation(&self) -> Option<&'static str> {
53        match self {
54            ComponentKind::Rule(rule) => Some(rule.docs()),
55            ComponentKind::CommandName(name) => match name.as_ref() {
56                "ignore" => Some(
57                    "`ignore` will ignore all rules, or any given rules in some range or node.",
58                ),
59                _ => None,
60            },
61            _ => None,
62        }
63    }
64}
65
66impl ComponentKind {
67    pub fn rule(&self) -> Option<Box<dyn CstRule>> {
68        match self {
69            ComponentKind::Rule(rule) => Some(rule.clone()),
70            _ => None,
71        }
72    }
73
74    pub fn command_name(&self) -> Option<&str> {
75        match self {
76            ComponentKind::CommandName(name) => Some(name.as_str()),
77            _ => None,
78        }
79    }
80
81    pub fn literal(&self) -> Option<&str> {
82        match self {
83            ComponentKind::Literal(val) => Some(*val),
84            _ => None,
85        }
86    }
87
88    pub fn number(&self) -> Option<u64> {
89        match self {
90            ComponentKind::Number(val) => Some(*val),
91            _ => None,
92        }
93    }
94
95    pub fn repetition(&self) -> Option<&[Component]> {
96        match self {
97            ComponentKind::Repetition(components) => Some(components.as_slice()),
98            _ => None,
99        }
100    }
101}
102
103/// A `Component` represents a parsed `Instruction`, that also has a span,
104/// so you can find the `Component` at any span in the directive.
105#[derive(Debug, Clone)]
106pub struct Component {
107    pub kind: ComponentKind,
108    pub range: TextRange,
109}
110
111/// `Instruction`s are used to add directives to the parser.
112///
113/// Directives are parsed based off all registered instructions.
114///
115/// # Example
116///
117/// To add an `ignore` rule, you can add the following instructions:
118/// ```ignore
119/// # use rslint_core::directives::Instruction::*;
120/// # fn main() {
121/// vec![
122///   CommandName("ignore"),
123///   Repetition(RuleName, ","),
124///   Optional(vec![
125///     Literal("until"),
126///     Either(Literal("eof"), Number)
127///   ])
128/// ]
129/// # }
130/// ```
131#[derive(Debug, Clone)]
132pub enum Instruction {
133    RuleName,
134    Number,
135
136    CommandName(&'static str),
137    Literal(&'static str),
138    Optional(Vec<Instruction>),
139    Repetition(Box<Instruction>, SyntaxKind),
140    Either(Box<Instruction>, Box<Instruction>),
141}
142
143/// Any command that is given to the linter using an inline comment.
144#[derive(Debug, Clone)]
145pub struct Directive {
146    /// The line number in which the directive comment was parsed.
147    pub line: usize,
148    pub comment: Comment,
149    pub components: Vec<Component>,
150    /// Contains the parsed `Command`, but is `None` if the `components`
151    /// failed to be parsed as a valid `Command`.
152    pub command: Option<Command>,
153}
154
155impl Directive {
156    /// Finds the component which contains the given index in his span.
157    pub fn component_at(&self, idx: TextSize) -> Option<&Component> {
158        self.components
159            .iter()
160            .find(|c| c.range.contains(idx))
161            .and_then(|component| {
162                if let ComponentKind::Repetition(components) = &component.kind {
163                    components.iter().find(|c| c.range.contains(idx))
164                } else {
165                    Some(component)
166                }
167            })
168    }
169}
170
171/// Apply file level directives to a store and add their respective diagnostics to the pool of diagnostics.
172/// for file level ignores this will clear all the rules from the store.
173///
174/// This method furthermore issues more contextual warnings like disabling a rule after
175/// the entire file has been disabled.
176pub fn apply_top_level_directives(
177    directives: &[Directive],
178    store: &mut CstRuleStore,
179    diagnostics: &mut Vec<DirectiveError>,
180    file_id: usize,
181) {
182    // TODO: More complex warnings, things like ignoring node directives because of file level directives
183
184    let mut ignored = vec![];
185    let mut cleared = None;
186
187    for directive in directives {
188        match &directive.command {
189            Some(Command::IgnoreFile) => {
190                store.rules.clear();
191                cleared = Some(directive.comment.token.text_range());
192            }
193            Some(Command::IgnoreFileRules(rules)) => {
194                ignored.push(directive.comment.token.text_range());
195                store
196                    .rules
197                    .retain(|rule| !rules.iter().any(|allowed| allowed.name() == rule.name()));
198            }
199            _ => {}
200        }
201    }
202
203    if let Some(range) = cleared {
204        for ignored_range in ignored {
205            let warn = Diagnostic::warning(
206                file_id,
207                "linter",
208                "ignoring redundant rule ignore directive",
209            )
210            .secondary(range, "this directive ignores all rules")
211            .primary(ignored_range, "this directive is ignored")
212            .unnecessary();
213
214            diagnostics.push(DirectiveError::new(warn, DirectiveErrorKind::Other));
215        }
216    }
217}
218
219pub fn apply_node_directives(
220    directives: &[Directive],
221    node: &SyntaxNode,
222    store: &CstRuleStore,
223) -> Option<CstRuleStore> {
224    let comment = node.first_token().and_then(|t| t.comment())?;
225    let directive = directives.iter().find(|dir| dir.comment == comment)?;
226    let mut store = store.clone();
227
228    match &directive.command {
229        Some(Command::IgnoreNode(_)) => {
230            store.rules.clear();
231        }
232        Some(Command::IgnoreNodeRules(_, rules)) => {
233            store
234                .rules
235                .retain(|rule| !rules.iter().any(|allowed| allowed.name() == rule.name()));
236        }
237        _ => {}
238    }
239    Some(store)
240}
241
242pub fn skip_node(directives: &[Directive], node: &SyntaxNode, rule: &dyn CstRule) -> bool {
243    if let Some(comment) = node.first_token().and_then(|t| t.comment()) {
244        if let Some(directive) = directives.iter().find(|dir| dir.comment == comment) {
245            match &directive.command {
246                Some(Command::IgnoreNode(_)) => {
247                    return true;
248                }
249                Some(Command::IgnoreNodeRules(_, rules)) => {
250                    if rules.iter().any(|allowed| allowed.name() == rule.name()) {
251                        return true;
252                    }
253                }
254                _ => {}
255            }
256        }
257    }
258    false
259}
260
261rule_tests! {
262    crate::groups::errors::NoEmpty::default(),
263    err: {
264        "{}",
265        "
266        // rslint-ignore no-empty
267        {}
268
269        {}
270        "
271    },
272    ok: {
273        "
274        // rslint-ignore no-empty
275
276        {}
277        ",
278        "
279        // rslint-ignore no-empty
280        {}
281        ",
282        "
283        // rslint-ignore 
284        {}
285        "
286    }
287}