diff options
-rw-r--r-- | bin/src/config.rs | 24 | ||||
-rw-r--r-- | bin/src/err.rs | 10 | ||||
-rw-r--r-- | bin/src/explain.rs | 15 | ||||
-rw-r--r-- | bin/src/main.rs | 5 | ||||
-rw-r--r-- | lib/src/lib.rs | 30 | ||||
-rw-r--r-- | lib/src/lints/bool_comparison.rs | 23 | ||||
-rw-r--r-- | lib/src/lints/collapsible_let_in.rs | 37 | ||||
-rw-r--r-- | lib/src/lints/empty_let_in.rs | 25 | ||||
-rw-r--r-- | lib/src/lints/empty_pattern.rs | 31 | ||||
-rw-r--r-- | lib/src/lints/eta_reduction.rs | 30 | ||||
-rw-r--r-- | lib/src/lints/legacy_let_syntax.rs | 32 | ||||
-rw-r--r-- | lib/src/lints/manual_inherit.rs | 28 | ||||
-rw-r--r-- | lib/src/lints/manual_inherit_from.rs | 28 | ||||
-rw-r--r-- | lib/src/lints/redundant_pattern_bind.rs | 25 | ||||
-rw-r--r-- | lib/src/lints/unquoted_splice.rs | 28 | ||||
-rw-r--r-- | lib/src/lints/useless_parens.rs | 35 | ||||
-rw-r--r-- | macros/src/explain.rs | 28 | ||||
-rw-r--r-- | macros/src/lib.rs | 173 | ||||
-rw-r--r-- | macros/src/metadata.rs | 177 |
19 files changed, 578 insertions, 206 deletions
diff --git a/bin/src/config.rs b/bin/src/config.rs index ef3a29d..1649017 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs | |||
@@ -26,6 +26,8 @@ pub enum SubCommand { | |||
26 | Fix(Fix), | 26 | Fix(Fix), |
27 | /// Fix exactly one issue at provided position | 27 | /// Fix exactly one issue at provided position |
28 | Single(Single), | 28 | Single(Single), |
29 | /// Print detailed explanation for a lint warning | ||
30 | Explain(Explain), | ||
29 | } | 31 | } |
30 | 32 | ||
31 | #[derive(Clap, Debug)] | 33 | #[derive(Clap, Debug)] |
@@ -88,6 +90,13 @@ pub struct Single { | |||
88 | pub diff_only: bool, | 90 | pub diff_only: bool, |
89 | } | 91 | } |
90 | 92 | ||
93 | #[derive(Clap, Debug)] | ||
94 | pub struct Explain { | ||
95 | /// Warning code to explain | ||
96 | #[clap(parse(try_from_str = parse_warning_code))] | ||
97 | pub target: u32, | ||
98 | } | ||
99 | |||
91 | mod dirs { | 100 | mod dirs { |
92 | use std::{ | 101 | use std::{ |
93 | fs, | 102 | fs, |
@@ -160,6 +169,21 @@ fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> { | |||
160 | } | 169 | } |
161 | } | 170 | } |
162 | 171 | ||
172 | fn parse_warning_code(src: &str) -> Result<u32, ConfigErr> { | ||
173 | let mut char_stream = src.chars(); | ||
174 | let severity = char_stream | ||
175 | .next() | ||
176 | .ok_or(ConfigErr::InvalidWarningCode(src.to_owned()))? | ||
177 | .to_ascii_lowercase(); | ||
178 | match severity { | ||
179 | 'w' => char_stream | ||
180 | .collect::<String>() | ||
181 | .parse::<u32>() | ||
182 | .map_err(|_| ConfigErr::InvalidWarningCode(src.to_owned())), | ||
183 | _ => Ok(0), | ||
184 | } | ||
185 | } | ||
186 | |||
163 | fn build_ignore_set(ignores: &[String]) -> Result<GlobSet, GlobError> { | 187 | fn build_ignore_set(ignores: &[String]) -> Result<GlobSet, GlobError> { |
164 | let mut set = GlobSetBuilder::new(); | 188 | let mut set = GlobSetBuilder::new(); |
165 | for pattern in ignores { | 189 | for pattern in ignores { |
diff --git a/bin/src/err.rs b/bin/src/err.rs index 4c16d69..1e52c2b 100644 --- a/bin/src/err.rs +++ b/bin/src/err.rs | |||
@@ -12,6 +12,8 @@ pub enum ConfigErr { | |||
12 | InvalidPath(#[from] io::Error), | 12 | InvalidPath(#[from] io::Error), |
13 | #[error("unable to parse `{0}` as line and column")] | 13 | #[error("unable to parse `{0}` as line and column")] |
14 | InvalidPosition(String), | 14 | InvalidPosition(String), |
15 | #[error("unable to parse `{0}` as warning code")] | ||
16 | InvalidWarningCode(String), | ||
15 | } | 17 | } |
16 | 18 | ||
17 | // #[derive(Error, Debug)] | 19 | // #[derive(Error, Debug)] |
@@ -41,6 +43,12 @@ pub enum SingleFixErr { | |||
41 | } | 43 | } |
42 | 44 | ||
43 | #[derive(Error, Debug)] | 45 | #[derive(Error, Debug)] |
46 | pub enum ExplainErr { | ||
47 | #[error("lint with code `{0}` not found")] | ||
48 | LintNotFound(u32), | ||
49 | } | ||
50 | |||
51 | #[derive(Error, Debug)] | ||
44 | pub enum StatixErr { | 52 | pub enum StatixErr { |
45 | // #[error("linter error: {0}")] | 53 | // #[error("linter error: {0}")] |
46 | // Lint(#[from] LintErr), | 54 | // Lint(#[from] LintErr), |
@@ -50,4 +58,6 @@ pub enum StatixErr { | |||
50 | Single(#[from] SingleFixErr), | 58 | Single(#[from] SingleFixErr), |
51 | #[error("config error: {0}")] | 59 | #[error("config error: {0}")] |
52 | Config(#[from] ConfigErr), | 60 | Config(#[from] ConfigErr), |
61 | #[error("explain error: {0}")] | ||
62 | Explain(#[from] ExplainErr), | ||
53 | } | 63 | } |
diff --git a/bin/src/explain.rs b/bin/src/explain.rs new file mode 100644 index 0000000..6aefa7e --- /dev/null +++ b/bin/src/explain.rs | |||
@@ -0,0 +1,15 @@ | |||
1 | use crate::err::ExplainErr; | ||
2 | |||
3 | use lib::LINTS; | ||
4 | |||
5 | pub fn explain(code: u32) -> Result<&'static str, ExplainErr> { | ||
6 | match code { | ||
7 | 0 => Ok("syntax error"), | ||
8 | _ => LINTS | ||
9 | .values() | ||
10 | .flatten() | ||
11 | .find(|l| l.code() == code) | ||
12 | .map(|l| l.explanation()) | ||
13 | .ok_or(ExplainErr::LintNotFound(code)), | ||
14 | } | ||
15 | } | ||
diff --git a/bin/src/main.rs b/bin/src/main.rs index 90b79ce..31f6823 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs | |||
@@ -1,5 +1,6 @@ | |||
1 | mod config; | 1 | mod config; |
2 | mod err; | 2 | mod err; |
3 | mod explain; | ||
3 | mod fix; | 4 | mod fix; |
4 | mod lint; | 5 | mod lint; |
5 | mod traits; | 6 | mod traits; |
@@ -86,6 +87,10 @@ fn _main() -> Result<(), StatixErr> { | |||
86 | print!("{}", &*single_fix_result.src) | 87 | print!("{}", &*single_fix_result.src) |
87 | } | 88 | } |
88 | } | 89 | } |
90 | SubCommand::Explain(explain_config) => { | ||
91 | let explanation = explain::explain(explain_config.target)?; | ||
92 | println!("{}", explanation) | ||
93 | } | ||
89 | } | 94 | } |
90 | Ok(()) | 95 | Ok(()) |
91 | } | 96 | } |
diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 196cbf8..5347666 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs | |||
@@ -226,25 +226,29 @@ pub trait Rule { | |||
226 | /// Contains information about the lint itself. Do not implement manually, | 226 | /// Contains information about the lint itself. Do not implement manually, |
227 | /// look at the `lint` attribute macro instead for implementing rules | 227 | /// look at the `lint` attribute macro instead for implementing rules |
228 | pub trait Metadata { | 228 | pub trait Metadata { |
229 | fn name() -> &'static str | 229 | fn name(&self) -> &'static str; |
230 | where | 230 | fn note(&self) -> &'static str; |
231 | Self: Sized; | 231 | fn code(&self) -> u32; |
232 | fn note() -> &'static str | 232 | fn report(&self) -> Report; |
233 | where | ||
234 | Self: Sized; | ||
235 | fn code() -> u32 | ||
236 | where | ||
237 | Self: Sized; | ||
238 | fn report() -> Report | ||
239 | where | ||
240 | Self: Sized; | ||
241 | fn match_with(&self, with: &SyntaxKind) -> bool; | 233 | fn match_with(&self, with: &SyntaxKind) -> bool; |
242 | fn match_kind(&self) -> Vec<SyntaxKind>; | 234 | fn match_kind(&self) -> Vec<SyntaxKind>; |
243 | } | 235 | } |
244 | 236 | ||
237 | /// Contains offline explanation for each lint | ||
238 | /// The `lint` macro scans nearby doc comments for | ||
239 | /// explanations and derives this trait. | ||
240 | /// | ||
241 | /// FIXME: the lint macro does way too much, maybe | ||
242 | /// split it into smaller macros. | ||
243 | pub trait Explain { | ||
244 | fn explanation(&self) -> &'static str { | ||
245 | "no explanation found" | ||
246 | } | ||
247 | } | ||
248 | |||
245 | /// Combines Rule and Metadata, do not implement manually, this is derived by | 249 | /// Combines Rule and Metadata, do not implement manually, this is derived by |
246 | /// the `lint` macro. | 250 | /// the `lint` macro. |
247 | pub trait Lint: Metadata + Rule + Send + Sync {} | 251 | pub trait Lint: Metadata + Explain + Rule + Send + Sync {} |
248 | 252 | ||
249 | /// Helper utility to take lints from modules and insert them into a map for efficient | 253 | /// Helper utility to take lints from modules and insert them into a map for efficient |
250 | /// access. Mapping is from a SyntaxKind to a list of lints that apply on that Kind. | 254 | /// access. Mapping is from a SyntaxKind to a list of lints that apply on that Kind. |
diff --git a/lib/src/lints/bool_comparison.rs b/lib/src/lints/bool_comparison.rs index 0b5733b..5c9bee8 100644 --- a/lib/src/lints/bool_comparison.rs +++ b/lib/src/lints/bool_comparison.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,25 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for expressions of the form `x == true`, `x != true` and | ||
12 | /// suggests using the variable directly. | ||
13 | /// | ||
14 | /// ## Why is this bad? | ||
15 | /// Unnecessary code. | ||
16 | /// | ||
17 | /// ## Example | ||
18 | /// Instead of checking the value of `x`: | ||
19 | /// | ||
20 | /// ``` | ||
21 | /// if x == true then 0 else 1 | ||
22 | /// ``` | ||
23 | /// | ||
24 | /// Use `x` directly: | ||
25 | /// | ||
26 | /// ``` | ||
27 | /// if x then 0 else 1 | ||
28 | /// ``` | ||
10 | #[lint( | 29 | #[lint( |
11 | name = "bool_comparison", | 30 | name = "bool_comparison", |
12 | note = "Unnecessary comparison with boolean", | 31 | note = "Unnecessary comparison with boolean", |
@@ -71,7 +90,7 @@ impl Rule for BoolComparison { | |||
71 | non_bool_side, | 90 | non_bool_side, |
72 | bool_side | 91 | bool_side |
73 | ); | 92 | ); |
74 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 93 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
75 | } else { | 94 | } else { |
76 | None | 95 | None |
77 | } | 96 | } |
diff --git a/lib/src/lints/collapsible_let_in.rs b/lib/src/lints/collapsible_let_in.rs index 878d218..21199a8 100644 --- a/lib/src/lints/collapsible_let_in.rs +++ b/lib/src/lints/collapsible_let_in.rs | |||
@@ -1,13 +1,41 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
5 | use rowan::Direction; | ||
6 | use rnix::{ | 5 | use rnix::{ |
7 | types::{LetIn, TypedNode}, | 6 | types::{LetIn, TypedNode}, |
8 | NodeOrToken, SyntaxElement, SyntaxKind, TextRange | 7 | NodeOrToken, SyntaxElement, SyntaxKind, TextRange, |
9 | }; | 8 | }; |
9 | use rowan::Direction; | ||
10 | 10 | ||
11 | /// ## What it does | ||
12 | /// Checks for `let-in` expressions whose body is another `let-in` | ||
13 | /// expression. | ||
14 | /// | ||
15 | /// ## Why is this bad? | ||
16 | /// Unnecessary code, the `let-in` expressions can be merged. | ||
17 | /// | ||
18 | /// ## Example | ||
19 | /// | ||
20 | /// ``` | ||
21 | /// let | ||
22 | /// a = 2; | ||
23 | /// in | ||
24 | /// let | ||
25 | /// b = 3; | ||
26 | /// in | ||
27 | /// a + b | ||
28 | /// ``` | ||
29 | /// | ||
30 | /// Merge both `let-in` expressions: | ||
31 | /// | ||
32 | /// ``` | ||
33 | /// let | ||
34 | /// a = 2; | ||
35 | /// b = 3; | ||
36 | /// in | ||
37 | /// a + b | ||
38 | /// ``` | ||
11 | #[lint( | 39 | #[lint( |
12 | name = "collapsible let in", | 40 | name = "collapsible let in", |
13 | note = "These let-in expressions are collapsible", | 41 | note = "These let-in expressions are collapsible", |
@@ -47,7 +75,7 @@ impl Rule for CollapsibleLetIn { | |||
47 | let replacement = make::empty().node().clone(); | 75 | let replacement = make::empty().node().clone(); |
48 | 76 | ||
49 | Some( | 77 | Some( |
50 | Self::report() | 78 | self.report() |
51 | .diagnostic(first_annotation, first_message) | 79 | .diagnostic(first_annotation, first_message) |
52 | .suggest(second_annotation, second_message, Suggestion::new(replacement_at, replacement)) | 80 | .suggest(second_annotation, second_message, Suggestion::new(replacement_at, replacement)) |
53 | ) | 81 | ) |
@@ -57,4 +85,3 @@ impl Rule for CollapsibleLetIn { | |||
57 | } | 85 | } |
58 | } | 86 | } |
59 | } | 87 | } |
60 | |||
diff --git a/lib/src/lints/empty_let_in.rs b/lib/src/lints/empty_let_in.rs index aae1377..b255c23 100644 --- a/lib/src/lints/empty_let_in.rs +++ b/lib/src/lints/empty_let_in.rs | |||
@@ -1,12 +1,30 @@ | |||
1 | use crate::{Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
5 | use rnix::{ | 5 | use rnix::{ |
6 | types::{LetIn, TypedNode, EntryHolder}, | 6 | types::{EntryHolder, LetIn, TypedNode}, |
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for `let-in` expressions which create no new bindings. | ||
12 | /// | ||
13 | /// ## Why is this bad? | ||
14 | /// `let-in` expressions that create no new bindings are useless. | ||
15 | /// These are probably remnants from debugging or editing expressions. | ||
16 | /// | ||
17 | /// ## Example | ||
18 | /// | ||
19 | /// ``` | ||
20 | /// let in pkgs.statix | ||
21 | /// ``` | ||
22 | /// | ||
23 | /// Preserve only the body of the `let-in` expression: | ||
24 | /// | ||
25 | /// ``` | ||
26 | /// pkgs.statix | ||
27 | /// ``` | ||
10 | #[lint( | 28 | #[lint( |
11 | name = "empty let-in", | 29 | name = "empty let-in", |
12 | note = "Useless let-in expression", | 30 | note = "Useless let-in expression", |
@@ -31,11 +49,10 @@ impl Rule for EmptyLetIn { | |||
31 | let at = node.text_range(); | 49 | let at = node.text_range(); |
32 | let replacement = body; | 50 | let replacement = body; |
33 | let message = "This let-in expression has no entries"; | 51 | let message = "This let-in expression has no entries"; |
34 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 52 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
35 | } else { | 53 | } else { |
36 | None | 54 | None |
37 | } | 55 | } |
38 | } | 56 | } |
39 | } | 57 | } |
40 | } | 58 | } |
41 | |||
diff --git a/lib/src/lints/empty_pattern.rs b/lib/src/lints/empty_pattern.rs index 6fb7558..5312548 100644 --- a/lib/src/lints/empty_pattern.rs +++ b/lib/src/lints/empty_pattern.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,33 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for an empty variadic pattern: `{...}`, in a function | ||
12 | /// argument. | ||
13 | /// | ||
14 | /// ## Why is this bad? | ||
15 | /// The intention with empty patterns is not instantly obvious. Prefer | ||
16 | /// an underscore identifier instead, to indicate that the argument | ||
17 | /// is being ignored. | ||
18 | /// | ||
19 | /// ## Example | ||
20 | /// | ||
21 | /// ``` | ||
22 | /// client = { ... }: { | ||
23 | /// imports = [ self.nixosModules.irmaseal-pkg ]; | ||
24 | /// services.irmaseal-pkg.enable = true; | ||
25 | /// }; | ||
26 | /// ``` | ||
27 | /// | ||
28 | /// Replace the empty variadic pattern with `_` to indicate that you | ||
29 | /// intend to ignore the argument: | ||
30 | /// | ||
31 | /// ``` | ||
32 | /// client = _: { | ||
33 | /// imports = [ self.nixosModules.irmaseal-pkg ]; | ||
34 | /// services.irmaseal-pkg.enable = true; | ||
35 | /// }; | ||
36 | /// ``` | ||
10 | #[lint( | 37 | #[lint( |
11 | name = "empty pattern", | 38 | name = "empty pattern", |
12 | note = "Found empty pattern in function argument", | 39 | note = "Found empty pattern in function argument", |
@@ -28,7 +55,7 @@ impl Rule for EmptyPattern { | |||
28 | let at = node.text_range(); | 55 | let at = node.text_range(); |
29 | let message = "This pattern is empty, use `_` instead"; | 56 | let message = "This pattern is empty, use `_` instead"; |
30 | let replacement = make::ident("_").node().clone(); | 57 | let replacement = make::ident("_").node().clone(); |
31 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 58 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
32 | } else { | 59 | } else { |
33 | None | 60 | None |
34 | } | 61 | } |
diff --git a/lib/src/lints/eta_reduction.rs b/lib/src/lints/eta_reduction.rs index 79a5101..3a483d0 100644 --- a/lib/src/lints/eta_reduction.rs +++ b/lib/src/lints/eta_reduction.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,32 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for eta-reducible functions, i.e.: converts lambda | ||
12 | /// expressions into free standing functions where applicable. | ||
13 | /// | ||
14 | /// ## Why is this bad? | ||
15 | /// Oftentimes, eta-reduction results in code that is more natural | ||
16 | /// to read. | ||
17 | /// | ||
18 | /// ## Example | ||
19 | /// | ||
20 | /// ``` | ||
21 | /// let | ||
22 | /// double = i: 2 * i; | ||
23 | /// in | ||
24 | /// map (x: double x) [ 1 2 3 ] | ||
25 | /// ``` | ||
26 | /// | ||
27 | /// The lambda passed to the `map` function is eta-reducible, and the | ||
28 | /// result reads more naturally: | ||
29 | /// | ||
30 | /// ``` | ||
31 | /// let | ||
32 | /// double = i: 2 * i; | ||
33 | /// in | ||
34 | /// map double [ 1 2 3 ] | ||
35 | /// ``` | ||
10 | #[lint( | 36 | #[lint( |
11 | name = "eta reduction", | 37 | name = "eta reduction", |
12 | note = "This function expression is eta reducible", | 38 | note = "This function expression is eta reducible", |
@@ -43,7 +69,7 @@ impl Rule for EtaReduction { | |||
43 | "Found eta-reduction: `{}`", | 69 | "Found eta-reduction: `{}`", |
44 | replacement.text().to_string() | 70 | replacement.text().to_string() |
45 | ); | 71 | ); |
46 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 72 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
47 | } else { | 73 | } else { |
48 | None | 74 | None |
49 | } | 75 | } |
diff --git a/lib/src/lints/legacy_let_syntax.rs b/lib/src/lints/legacy_let_syntax.rs index 2087e27..139f633 100644 --- a/lib/src/lints/legacy_let_syntax.rs +++ b/lib/src/lints/legacy_let_syntax.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,34 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for legacy-let syntax that was never formalized. | ||
12 | /// | ||
13 | /// ## Why is this bad? | ||
14 | /// This syntax construct is undocumented, refrain from using it. | ||
15 | /// | ||
16 | /// ## Example | ||
17 | /// | ||
18 | /// Legacy let syntax makes use of an attribute set annotated with | ||
19 | /// `let` and expects a `body` attribute. | ||
20 | /// ``` | ||
21 | /// let { | ||
22 | /// body = x + y; | ||
23 | /// x = 2; | ||
24 | /// y = 3; | ||
25 | /// } | ||
26 | /// ``` | ||
27 | /// | ||
28 | /// This is trivially representible via `rec`, which is documented | ||
29 | /// and more widely known: | ||
30 | /// | ||
31 | /// ``` | ||
32 | /// rec { | ||
33 | /// body = x + y; | ||
34 | /// x = 2; | ||
35 | /// y = 3; | ||
36 | /// }.body | ||
37 | /// ``` | ||
10 | #[lint( | 38 | #[lint( |
11 | name = "legacy let syntax", | 39 | name = "legacy let syntax", |
12 | note = "Using undocumented `let` syntax", | 40 | note = "Using undocumented `let` syntax", |
@@ -36,7 +64,7 @@ impl Rule for ManualInherit { | |||
36 | let message = "Prefer `rec` over undocumented `let` syntax"; | 64 | let message = "Prefer `rec` over undocumented `let` syntax"; |
37 | let replacement = selected.node().clone(); | 65 | let replacement = selected.node().clone(); |
38 | 66 | ||
39 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 67 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
40 | } else { | 68 | } else { |
41 | None | 69 | None |
42 | } | 70 | } |
diff --git a/lib/src/lints/manual_inherit.rs b/lib/src/lints/manual_inherit.rs index 0a6933c..2d119c3 100644 --- a/lib/src/lints/manual_inherit.rs +++ b/lib/src/lints/manual_inherit.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,30 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for bindings of the form `a = a`. | ||
12 | /// | ||
13 | /// ## Why is this bad? | ||
14 | /// If the aim is to bring attributes from a larger scope into | ||
15 | /// the current scope, prefer an inherit statement. | ||
16 | /// | ||
17 | /// ## Example | ||
18 | /// | ||
19 | /// ``` | ||
20 | /// let | ||
21 | /// a = 2; | ||
22 | /// in | ||
23 | /// { a = a; b = 3; } | ||
24 | /// ``` | ||
25 | /// | ||
26 | /// Try `inherit` instead: | ||
27 | /// | ||
28 | /// ``` | ||
29 | /// let | ||
30 | /// a = 2; | ||
31 | /// in | ||
32 | /// { inherit a; b = 3; } | ||
33 | /// ``` | ||
10 | #[lint( | 34 | #[lint( |
11 | name = "manual inherit", | 35 | name = "manual inherit", |
12 | note = "Assignment instead of inherit", | 36 | note = "Assignment instead of inherit", |
@@ -35,7 +59,7 @@ impl Rule for ManualInherit { | |||
35 | let at = node.text_range(); | 59 | let at = node.text_range(); |
36 | let replacement = make::inherit_stmt(&[key]).node().clone(); | 60 | let replacement = make::inherit_stmt(&[key]).node().clone(); |
37 | let message = "This assignment is better written with `inherit`"; | 61 | let message = "This assignment is better written with `inherit`"; |
38 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 62 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
39 | } else { | 63 | } else { |
40 | None | 64 | None |
41 | } | 65 | } |
diff --git a/lib/src/lints/manual_inherit_from.rs b/lib/src/lints/manual_inherit_from.rs index 794aaf9..8d0f539 100644 --- a/lib/src/lints/manual_inherit_from.rs +++ b/lib/src/lints/manual_inherit_from.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,30 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for bindings of the form `a = someAttr.a`. | ||
12 | /// | ||
13 | /// ## Why is this bad? | ||
14 | /// If the aim is to extract or bring attributes of an attrset into | ||
15 | /// scope, prefer an inherit statement. | ||
16 | /// | ||
17 | /// ## Example | ||
18 | /// | ||
19 | /// ``` | ||
20 | /// let | ||
21 | /// mtl = pkgs.haskellPackages.mtl; | ||
22 | /// in | ||
23 | /// null | ||
24 | /// ``` | ||
25 | /// | ||
26 | /// Try `inherit` instead: | ||
27 | /// | ||
28 | /// ``` | ||
29 | /// let | ||
30 | /// inherit (pkgs.haskellPackages) mtl; | ||
31 | /// in | ||
32 | /// null | ||
33 | /// ``` | ||
10 | #[lint( | 34 | #[lint( |
11 | name = "manual inherit from", | 35 | name = "manual inherit from", |
12 | note = "Assignment instead of inherit from", | 36 | note = "Assignment instead of inherit from", |
@@ -40,7 +64,7 @@ impl Rule for ManualInherit { | |||
40 | make::inherit_from_stmt(set, &[key]).node().clone() | 64 | make::inherit_from_stmt(set, &[key]).node().clone() |
41 | }; | 65 | }; |
42 | let message = "This assignment is better written with `inherit`"; | 66 | let message = "This assignment is better written with `inherit`"; |
43 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 67 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
44 | } else { | 68 | } else { |
45 | None | 69 | None |
46 | } | 70 | } |
diff --git a/lib/src/lints/redundant_pattern_bind.rs b/lib/src/lints/redundant_pattern_bind.rs index aebc549..5b0711f 100644 --- a/lib/src/lints/redundant_pattern_bind.rs +++ b/lib/src/lints/redundant_pattern_bind.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,10 +7,29 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for binds of the form `inputs @ { ... }` in function | ||
12 | /// arguments. | ||
13 | /// | ||
14 | /// ## Why is this bad? | ||
15 | /// The variadic pattern here is redundant, as it does not capture | ||
16 | /// anything. | ||
17 | /// | ||
18 | /// ## Example | ||
19 | /// | ||
20 | /// ``` | ||
21 | /// inputs @ { ... }: inputs.nixpkgs | ||
22 | /// ``` | ||
23 | /// | ||
24 | /// Remove the pattern altogether: | ||
25 | /// | ||
26 | /// ``` | ||
27 | /// inputs: inputs.nixpkgs | ||
28 | /// ``` | ||
10 | #[lint( | 29 | #[lint( |
11 | name = "redundant pattern bind", | 30 | name = "redundant pattern bind", |
12 | note = "Found redundant pattern bind in function argument", | 31 | note = "Found redundant pattern bind in function argument", |
13 | code = 10, | 32 | code = 11, |
14 | match_with = SyntaxKind::NODE_PATTERN | 33 | match_with = SyntaxKind::NODE_PATTERN |
15 | )] | 34 | )] |
16 | struct RedundantPatternBind; | 35 | struct RedundantPatternBind; |
@@ -32,7 +51,7 @@ impl Rule for RedundantPatternBind { | |||
32 | let at = node.text_range(); | 51 | let at = node.text_range(); |
33 | let message = format!("This pattern bind is redundant, use `{}` instead", ident.as_str()); | 52 | let message = format!("This pattern bind is redundant, use `{}` instead", ident.as_str()); |
34 | let replacement = ident.node().clone(); | 53 | let replacement = ident.node().clone(); |
35 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 54 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
36 | } else { | 55 | } else { |
37 | None | 56 | None |
38 | } | 57 | } |
diff --git a/lib/src/lints/unquoted_splice.rs b/lib/src/lints/unquoted_splice.rs index 4d1ed69..c2fd6e4 100644 --- a/lib/src/lints/unquoted_splice.rs +++ b/lib/src/lints/unquoted_splice.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; | 1 | use crate::{make, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
@@ -7,6 +7,30 @@ use rnix::{ | |||
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for antiquote/splice expressions that are not quoted. | ||
12 | /// | ||
13 | /// ## Why is this bad? | ||
14 | /// An *anti*quoted expression should always occur within a *quoted* | ||
15 | /// expression. | ||
16 | /// | ||
17 | /// ## Example | ||
18 | /// | ||
19 | /// ``` | ||
20 | /// let | ||
21 | /// pkgs = nixpkgs.legacyPackages.${system}; | ||
22 | /// in | ||
23 | /// pkgs | ||
24 | /// ``` | ||
25 | /// | ||
26 | /// Quote the splice expression: | ||
27 | /// | ||
28 | /// ``` | ||
29 | /// let | ||
30 | /// pkgs = nixpkgs.legacyPackages."${system}"; | ||
31 | /// in | ||
32 | /// pkgs | ||
33 | /// ``` | ||
10 | #[lint( | 34 | #[lint( |
11 | name = "unquoted splice", | 35 | name = "unquoted splice", |
12 | note = "Found unquoted splice expression", | 36 | note = "Found unquoted splice expression", |
@@ -24,7 +48,7 @@ impl Rule for UnquotedSplice { | |||
24 | let at = node.text_range(); | 48 | let at = node.text_range(); |
25 | let replacement = make::quote(&node).node().clone(); | 49 | let replacement = make::quote(&node).node().clone(); |
26 | let message = "Consider quoting this splice expression"; | 50 | let message = "Consider quoting this splice expression"; |
27 | Some(Self::report().suggest(at, message, Suggestion::new(at, replacement))) | 51 | Some(self.report().suggest(at, message, Suggestion::new(at, replacement))) |
28 | } else { | 52 | } else { |
29 | None | 53 | None |
30 | } | 54 | } |
diff --git a/lib/src/lints/useless_parens.rs b/lib/src/lints/useless_parens.rs index 2d6ba8f..36ad1b7 100644 --- a/lib/src/lints/useless_parens.rs +++ b/lib/src/lints/useless_parens.rs | |||
@@ -1,12 +1,37 @@ | |||
1 | use crate::{Lint, Metadata, Report, Rule, Suggestion, Diagnostic}; | 1 | use crate::{Diagnostic, Metadata, Report, Rule, Suggestion}; |
2 | 2 | ||
3 | use if_chain::if_chain; | 3 | use if_chain::if_chain; |
4 | use macros::lint; | 4 | use macros::lint; |
5 | use rnix::{ | 5 | use rnix::{ |
6 | types::{ParsedType, KeyValue, Paren, TypedNode, Wrapper}, | 6 | types::{KeyValue, Paren, ParsedType, TypedNode, Wrapper}, |
7 | NodeOrToken, SyntaxElement, SyntaxKind, | 7 | NodeOrToken, SyntaxElement, SyntaxKind, |
8 | }; | 8 | }; |
9 | 9 | ||
10 | /// ## What it does | ||
11 | /// Checks for unnecessary parentheses. | ||
12 | /// | ||
13 | /// ## Why is this bad? | ||
14 | /// Unnecessarily parenthesized code is hard to read. | ||
15 | /// | ||
16 | /// ## Example | ||
17 | /// | ||
18 | /// ``` | ||
19 | /// let | ||
20 | /// double = (x: 2 * x); | ||
21 | /// ls = map (double) [ 1 2 3 ]; | ||
22 | /// in | ||
23 | /// (2 + 3) | ||
24 | /// ``` | ||
25 | /// | ||
26 | /// Remove unnecessary parentheses: | ||
27 | /// | ||
28 | /// ``` | ||
29 | /// let | ||
30 | /// double = x: 2 * x; | ||
31 | /// ls = map double [ 1 2 3 ]; | ||
32 | /// in | ||
33 | /// 2 + 3 | ||
34 | /// ``` | ||
10 | #[lint( | 35 | #[lint( |
11 | name = "useless parens", | 36 | name = "useless parens", |
12 | note = "These parentheses can be omitted", | 37 | note = "These parentheses can be omitted", |
@@ -27,7 +52,7 @@ impl Rule for UselessParens { | |||
27 | 52 | ||
28 | if let Some(diagnostic) = do_thing(parsed_type_node); | 53 | if let Some(diagnostic) = do_thing(parsed_type_node); |
29 | then { | 54 | then { |
30 | let mut report = Self::report(); | 55 | let mut report = self.report(); |
31 | report.diagnostics.push(diagnostic); | 56 | report.diagnostics.push(diagnostic); |
32 | Some(report) | 57 | Some(report) |
33 | } else { | 58 | } else { |
@@ -79,7 +104,7 @@ fn do_thing(parsed_type_node: ParsedType) -> Option<Diagnostic> { | |||
79 | if let Some(parsed_inner) = ParsedType::cast(inner_node); | 104 | if let Some(parsed_inner) = ParsedType::cast(inner_node); |
80 | if matches!( | 105 | if matches!( |
81 | parsed_inner, | 106 | parsed_inner, |
82 | ParsedType::List(_) | 107 | ParsedType::List(_) |
83 | | ParsedType::Paren(_) | 108 | | ParsedType::Paren(_) |
84 | | ParsedType::Str(_) | 109 | | ParsedType::Str(_) |
85 | | ParsedType::AttrSet(_) | 110 | | ParsedType::AttrSet(_) |
@@ -95,6 +120,6 @@ fn do_thing(parsed_type_node: ParsedType) -> Option<Diagnostic> { | |||
95 | None | 120 | None |
96 | } | 121 | } |
97 | }, | 122 | }, |
98 | _ => None | 123 | _ => None, |
99 | } | 124 | } |
100 | } | 125 | } |
diff --git a/macros/src/explain.rs b/macros/src/explain.rs new file mode 100644 index 0000000..41dc5d4 --- /dev/null +++ b/macros/src/explain.rs | |||
@@ -0,0 +1,28 @@ | |||
1 | use proc_macro2::TokenStream as TokenStream2; | ||
2 | use quote::quote; | ||
3 | use syn::{ItemStruct, Lit, Meta, MetaNameValue}; | ||
4 | |||
5 | pub fn generate_explain_impl(struct_item: &ItemStruct) -> TokenStream2 { | ||
6 | let struct_name = &struct_item.ident; | ||
7 | let explain = struct_item | ||
8 | .attrs | ||
9 | .iter() | ||
10 | .filter_map(|attr| match attr.parse_meta().ok() { | ||
11 | Some(Meta::NameValue(MetaNameValue { | ||
12 | path, | ||
13 | lit: Lit::Str(str_lit), | ||
14 | .. | ||
15 | })) if path.is_ident("doc") => Some(str_lit.value()), | ||
16 | _ => None, | ||
17 | }) | ||
18 | .map(|s| s.strip_prefix(' ').unwrap_or(&s).to_owned()) | ||
19 | .collect::<Vec<_>>() | ||
20 | .join("\n"); | ||
21 | quote! { | ||
22 | impl crate::Explain for #struct_name { | ||
23 | fn explanation(&self) -> &'static str { | ||
24 | #explain | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | } | ||
diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 127b4cb..86fa509 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs | |||
@@ -1,44 +1,12 @@ | |||
1 | use std::collections::HashMap; | 1 | mod explain; |
2 | mod metadata; | ||
2 | 3 | ||
4 | use explain::generate_explain_impl; | ||
5 | use metadata::{generate_meta_impl, RawLintMeta}; | ||
3 | use proc_macro::TokenStream; | 6 | use proc_macro::TokenStream; |
4 | use proc_macro2::TokenStream as TokenStream2; | 7 | use proc_macro2::TokenStream as TokenStream2; |
5 | 8 | use quote::quote; | |
6 | use quote::{format_ident, quote}; | 9 | use syn::{parse_macro_input, Ident, ItemStruct}; |
7 | use syn::{ | ||
8 | parse::{Parse, ParseStream, Result as ParseResult}, | ||
9 | parse_macro_input, | ||
10 | punctuated::Punctuated, | ||
11 | Expr, Ident, ItemStruct, Lit, Token, | ||
12 | }; | ||
13 | |||
14 | struct KeyValue { | ||
15 | key: Ident, | ||
16 | _eq: Token![=], | ||
17 | value: Expr, | ||
18 | } | ||
19 | |||
20 | impl Parse for KeyValue { | ||
21 | fn parse(input: ParseStream) -> ParseResult<Self> { | ||
22 | Ok(Self { | ||
23 | key: input.parse()?, | ||
24 | _eq: input.parse()?, | ||
25 | value: input.parse()?, | ||
26 | }) | ||
27 | } | ||
28 | } | ||
29 | |||
30 | struct LintMeta(HashMap<Ident, Expr>); | ||
31 | |||
32 | impl Parse for LintMeta { | ||
33 | fn parse(input: ParseStream) -> ParseResult<Self> { | ||
34 | Ok(Self( | ||
35 | Punctuated::<KeyValue, Token![,]>::parse_terminated(input)? | ||
36 | .into_iter() | ||
37 | .map(|item| (item.key, item.value)) | ||
38 | .collect(), | ||
39 | )) | ||
40 | } | ||
41 | } | ||
42 | 10 | ||
43 | fn generate_self_impl(struct_name: &Ident) -> TokenStream2 { | 11 | fn generate_self_impl(struct_name: &Ident) -> TokenStream2 { |
44 | quote! { | 12 | quote! { |
@@ -50,136 +18,16 @@ fn generate_self_impl(struct_name: &Ident) -> TokenStream2 { | |||
50 | } | 18 | } |
51 | } | 19 | } |
52 | 20 | ||
53 | fn generate_meta_impl(struct_name: &Ident, meta: &LintMeta) -> TokenStream2 { | ||
54 | let name_fn = generate_name_fn(meta); | ||
55 | let note_fn = generate_note_fn(meta); | ||
56 | let code_fn = generate_code_fn(meta); | ||
57 | let report_fn = generate_report_fn(); | ||
58 | let match_with_fn = generate_match_with_fn(meta); | ||
59 | let match_kind = generate_match_kind(meta); | ||
60 | quote! { | ||
61 | impl Metadata for #struct_name { | ||
62 | #name_fn | ||
63 | #note_fn | ||
64 | #code_fn | ||
65 | #report_fn | ||
66 | #match_with_fn | ||
67 | #match_kind | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | |||
72 | fn generate_name_fn(meta: &LintMeta) -> TokenStream2 { | ||
73 | let name = meta | ||
74 | .0 | ||
75 | .get(&format_ident!("name")) | ||
76 | .unwrap_or_else(|| panic!("`name` not present")); | ||
77 | if let syn::Expr::Lit(name_lit) = name { | ||
78 | if let Lit::Str(name_str) = &name_lit.lit { | ||
79 | return quote! { | ||
80 | fn name() -> &'static str { | ||
81 | #name_str | ||
82 | } | ||
83 | }; | ||
84 | } | ||
85 | } | ||
86 | panic!("Invalid value for `name`"); | ||
87 | } | ||
88 | |||
89 | fn generate_note_fn(meta: &LintMeta) -> TokenStream2 { | ||
90 | let note = meta | ||
91 | .0 | ||
92 | .get(&format_ident!("note")) | ||
93 | .unwrap_or_else(|| panic!("`note` not present")); | ||
94 | if let syn::Expr::Lit(note_lit) = note { | ||
95 | if let Lit::Str(note_str) = ¬e_lit.lit { | ||
96 | return quote! { | ||
97 | fn note() -> &'static str { | ||
98 | #note_str | ||
99 | } | ||
100 | }; | ||
101 | } | ||
102 | } | ||
103 | panic!("Invalid value for `note`"); | ||
104 | } | ||
105 | |||
106 | fn generate_code_fn(meta: &LintMeta) -> TokenStream2 { | ||
107 | let code = meta | ||
108 | .0 | ||
109 | .get(&format_ident!("code")) | ||
110 | .unwrap_or_else(|| panic!("`code` not present")); | ||
111 | if let syn::Expr::Lit(code_lit) = code { | ||
112 | if let Lit::Int(code_int) = &code_lit.lit { | ||
113 | return quote! { | ||
114 | fn code() -> u32 { | ||
115 | #code_int | ||
116 | } | ||
117 | }; | ||
118 | } | ||
119 | } | ||
120 | panic!("Invalid value for `note`"); | ||
121 | } | ||
122 | |||
123 | fn generate_report_fn() -> TokenStream2 { | ||
124 | quote! { | ||
125 | fn report() -> Report { | ||
126 | Report::new(Self::note(), Self::code()) | ||
127 | } | ||
128 | } | ||
129 | } | ||
130 | |||
131 | fn generate_match_with_fn(meta: &LintMeta) -> TokenStream2 { | ||
132 | let match_with_lit = meta | ||
133 | .0 | ||
134 | .get(&format_ident!("match_with")) | ||
135 | .unwrap_or_else(|| panic!("`match_with` not present")); | ||
136 | if let syn::Expr::Path(match_path) = match_with_lit { | ||
137 | quote! { | ||
138 | fn match_with(&self, with: &SyntaxKind) -> bool { | ||
139 | #match_path == *with | ||
140 | } | ||
141 | } | ||
142 | } else if let syn::Expr::Array(array_expr) = match_with_lit { | ||
143 | quote! { | ||
144 | fn match_with(&self, with: &SyntaxKind) -> bool { | ||
145 | #array_expr.contains(with) | ||
146 | } | ||
147 | } | ||
148 | } else { | ||
149 | panic!("`match_with` has non-path value") | ||
150 | } | ||
151 | } | ||
152 | |||
153 | fn generate_match_kind(meta: &LintMeta) -> TokenStream2 { | ||
154 | let match_with_lit = meta | ||
155 | .0 | ||
156 | .get(&format_ident!("match_with")) | ||
157 | .unwrap_or_else(|| panic!("`match_with` not present")); | ||
158 | if let syn::Expr::Path(match_path) = match_with_lit { | ||
159 | quote! { | ||
160 | fn match_kind(&self) -> Vec<SyntaxKind> { | ||
161 | vec![#match_path] | ||
162 | } | ||
163 | } | ||
164 | } else if let syn::Expr::Array(array_expr) = match_with_lit { | ||
165 | quote! { | ||
166 | fn match_kind(&self) -> Vec<SyntaxKind> { | ||
167 | #array_expr.to_vec() | ||
168 | } | ||
169 | } | ||
170 | } else { | ||
171 | panic!("`match_with` has non-path value") | ||
172 | } | ||
173 | } | ||
174 | |||
175 | #[proc_macro_attribute] | 21 | #[proc_macro_attribute] |
176 | pub fn lint(attr: TokenStream, item: TokenStream) -> TokenStream { | 22 | pub fn lint(attr: TokenStream, item: TokenStream) -> TokenStream { |
177 | let struct_item = parse_macro_input!(item as ItemStruct); | 23 | let struct_item = parse_macro_input!(item as ItemStruct); |
178 | let meta = parse_macro_input!(attr as LintMeta); | 24 | let meta = parse_macro_input!(attr as RawLintMeta); |
179 | 25 | ||
180 | let struct_name = &struct_item.ident; | 26 | let struct_name = &struct_item.ident; |
181 | let self_impl = generate_self_impl(struct_name); | 27 | let self_impl = generate_self_impl(struct_name); |
182 | let meta_impl = generate_meta_impl(struct_name, &meta); | 28 | let meta_impl = generate_meta_impl(struct_name, &meta); |
29 | let explain_impl = generate_explain_impl(&struct_item); | ||
30 | |||
183 | (quote! { | 31 | (quote! { |
184 | #struct_item | 32 | #struct_item |
185 | 33 | ||
@@ -189,8 +37,9 @@ pub fn lint(attr: TokenStream, item: TokenStream) -> TokenStream { | |||
189 | 37 | ||
190 | #self_impl | 38 | #self_impl |
191 | #meta_impl | 39 | #meta_impl |
40 | #explain_impl | ||
192 | 41 | ||
193 | impl Lint for #struct_name {} | 42 | impl crate::Lint for #struct_name {} |
194 | }) | 43 | }) |
195 | .into() | 44 | .into() |
196 | } | 45 | } |
diff --git a/macros/src/metadata.rs b/macros/src/metadata.rs new file mode 100644 index 0000000..b41eb9c --- /dev/null +++ b/macros/src/metadata.rs | |||
@@ -0,0 +1,177 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | use proc_macro2::TokenStream as TokenStream2; | ||
4 | use quote::{format_ident, quote}; | ||
5 | use syn::{ | ||
6 | parse::{Parse, ParseStream, Result}, | ||
7 | punctuated::Punctuated, | ||
8 | Expr, ExprArray, Ident, Lit, Path, Token, | ||
9 | }; | ||
10 | |||
11 | struct KeyValue { | ||
12 | key: Ident, | ||
13 | _eq: Token![=], | ||
14 | value: Expr, | ||
15 | } | ||
16 | |||
17 | impl Parse for KeyValue { | ||
18 | fn parse(input: ParseStream) -> Result<Self> { | ||
19 | Ok(Self { | ||
20 | key: input.parse()?, | ||
21 | _eq: input.parse()?, | ||
22 | value: input.parse()?, | ||
23 | }) | ||
24 | } | ||
25 | } | ||
26 | |||
27 | pub struct RawLintMeta(HashMap<Ident, Expr>); | ||
28 | |||
29 | impl Parse for RawLintMeta { | ||
30 | fn parse(input: ParseStream) -> Result<Self> { | ||
31 | Ok(Self( | ||
32 | Punctuated::<KeyValue, Token![,]>::parse_terminated(input)? | ||
33 | .into_iter() | ||
34 | .map(|item| (item.key, item.value)) | ||
35 | .collect(), | ||
36 | )) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | pub struct LintMeta<'μ> { | ||
41 | name: &'μ Lit, | ||
42 | note: &'μ Lit, | ||
43 | code: &'μ Lit, | ||
44 | match_with: MatchWith<'μ>, | ||
45 | } | ||
46 | |||
47 | enum MatchWith<'π> { | ||
48 | Path(&'π Path), | ||
49 | Array(&'π ExprArray), | ||
50 | } | ||
51 | |||
52 | fn extract<'λ>(id: &str, raw: &'λ RawLintMeta) -> &'λ Expr { | ||
53 | raw.0 | ||
54 | .get(&format_ident!("{}", id)) | ||
55 | .unwrap_or_else(|| panic!("`{}` not present", id)) | ||
56 | } | ||
57 | |||
58 | fn as_lit(e: &Expr) -> &Lit { | ||
59 | match e { | ||
60 | Expr::Lit(l) => &l.lit, | ||
61 | _ => panic!("expected a literal"), | ||
62 | } | ||
63 | } | ||
64 | |||
65 | impl<'μ> LintMeta<'μ> { | ||
66 | fn from_raw(raw: &'μ RawLintMeta) -> Self { | ||
67 | let name = as_lit(extract("name", raw)); | ||
68 | let note = as_lit(extract("note", raw)); | ||
69 | let code = as_lit(extract("code", raw)); | ||
70 | let match_with_expr = extract("match_with", raw); | ||
71 | let match_with = match match_with_expr { | ||
72 | Expr::Path(p) => MatchWith::Path(&p.path), | ||
73 | Expr::Array(a) => MatchWith::Array(a), | ||
74 | _ => panic!("`match_with` is neither a path nor an array"), | ||
75 | }; | ||
76 | Self { | ||
77 | name, | ||
78 | note, | ||
79 | code, | ||
80 | match_with, | ||
81 | } | ||
82 | } | ||
83 | |||
84 | fn generate_name_fn(&self) -> TokenStream2 { | ||
85 | let name_str = self.name; | ||
86 | return quote! { | ||
87 | fn name(&self) -> &'static str { | ||
88 | #name_str | ||
89 | } | ||
90 | }; | ||
91 | } | ||
92 | |||
93 | fn generate_note_fn(&self) -> TokenStream2 { | ||
94 | let note_str = self.note; | ||
95 | return quote! { | ||
96 | fn note(&self) -> &'static str { | ||
97 | #note_str | ||
98 | } | ||
99 | }; | ||
100 | } | ||
101 | |||
102 | fn generate_code_fn(&self) -> TokenStream2 { | ||
103 | let code_int = self.code; | ||
104 | return quote! { | ||
105 | fn code(&self) -> u32 { | ||
106 | #code_int | ||
107 | } | ||
108 | }; | ||
109 | } | ||
110 | |||
111 | fn generate_match_with_fn(&self) -> TokenStream2 { | ||
112 | match self.match_with { | ||
113 | MatchWith::Path(p) => { | ||
114 | quote! { | ||
115 | fn match_with(&self, with: &SyntaxKind) -> bool { | ||
116 | #p == *with | ||
117 | } | ||
118 | } | ||
119 | } | ||
120 | MatchWith::Array(a) => { | ||
121 | quote! { | ||
122 | fn match_with(&self, with: &SyntaxKind) -> bool { | ||
123 | #a.contains(with) | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | fn generate_match_kind_fn(&self) -> TokenStream2 { | ||
131 | match self.match_with { | ||
132 | MatchWith::Path(p) => { | ||
133 | quote! { | ||
134 | fn match_kind(&self) -> Vec<SyntaxKind> { | ||
135 | vec![#p] | ||
136 | } | ||
137 | } | ||
138 | } | ||
139 | MatchWith::Array(a) => { | ||
140 | quote! { | ||
141 | fn match_kind(&self) -> Vec<SyntaxKind> { | ||
142 | #a.to_vec() | ||
143 | } | ||
144 | } | ||
145 | } | ||
146 | } | ||
147 | } | ||
148 | |||
149 | fn generate_report_fn(&self) -> TokenStream2 { | ||
150 | quote! { | ||
151 | fn report(&self) -> crate::Report { | ||
152 | crate::Report::new(self.note(), self.code()) | ||
153 | } | ||
154 | } | ||
155 | } | ||
156 | } | ||
157 | |||
158 | pub fn generate_meta_impl(struct_name: &Ident, meta: &RawLintMeta) -> TokenStream2 { | ||
159 | let not_raw = LintMeta::from_raw(&meta); | ||
160 | let name_fn = not_raw.generate_name_fn(); | ||
161 | let note_fn = not_raw.generate_note_fn(); | ||
162 | let code_fn = not_raw.generate_code_fn(); | ||
163 | let match_with_fn = not_raw.generate_match_with_fn(); | ||
164 | let match_kind = not_raw.generate_match_kind_fn(); | ||
165 | let report_fn = not_raw.generate_report_fn(); | ||
166 | |||
167 | quote! { | ||
168 | impl crate::Metadata for #struct_name { | ||
169 | #name_fn | ||
170 | #note_fn | ||
171 | #code_fn | ||
172 | #match_with_fn | ||
173 | #match_kind | ||
174 | #report_fn | ||
175 | } | ||
176 | } | ||
177 | } | ||