aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay <[email protected]>2021-10-31 09:05:26 +0000
committerAkshay <[email protected]>2021-10-31 16:05:15 +0000
commite8c955da4cbb042e6f9b89307d143f5bfa6779fa (patch)
tree0ae4ec11fd3dc0f8b69bc0f32c08858ef23a9485
parent246c69f8cfc74cf4c56fdaceaeb0562ed1f3dad5 (diff)
add `explain` subcommand and explanations to all lints
-rw-r--r--bin/src/config.rs24
-rw-r--r--bin/src/err.rs10
-rw-r--r--bin/src/explain.rs15
-rw-r--r--bin/src/main.rs5
-rw-r--r--lib/src/lib.rs30
-rw-r--r--lib/src/lints/bool_comparison.rs23
-rw-r--r--lib/src/lints/collapsible_let_in.rs37
-rw-r--r--lib/src/lints/empty_let_in.rs25
-rw-r--r--lib/src/lints/empty_pattern.rs31
-rw-r--r--lib/src/lints/eta_reduction.rs30
-rw-r--r--lib/src/lints/legacy_let_syntax.rs32
-rw-r--r--lib/src/lints/manual_inherit.rs28
-rw-r--r--lib/src/lints/manual_inherit_from.rs28
-rw-r--r--lib/src/lints/redundant_pattern_bind.rs25
-rw-r--r--lib/src/lints/unquoted_splice.rs28
-rw-r--r--lib/src/lints/useless_parens.rs35
-rw-r--r--macros/src/explain.rs28
-rw-r--r--macros/src/lib.rs173
-rw-r--r--macros/src/metadata.rs177
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)]
94pub struct Explain {
95 /// Warning code to explain
96 #[clap(parse(try_from_str = parse_warning_code))]
97 pub target: u32,
98}
99
91mod dirs { 100mod 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
172fn 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
163fn build_ignore_set(ignores: &[String]) -> Result<GlobSet, GlobError> { 187fn 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)]
46pub enum ExplainErr {
47 #[error("lint with code `{0}` not found")]
48 LintNotFound(u32),
49}
50
51#[derive(Error, Debug)]
44pub enum StatixErr { 52pub 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 @@
1use crate::err::ExplainErr;
2
3use lib::LINTS;
4
5pub 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 @@
1mod config; 1mod config;
2mod err; 2mod err;
3mod explain;
3mod fix; 4mod fix;
4mod lint; 5mod lint;
5mod traits; 6mod 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
228pub trait Metadata { 228pub 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.
243pub 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.
247pub trait Lint: Metadata + Rule + Send + Sync {} 251pub 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use macros::lint;
5use rowan::Direction;
6use rnix::{ 5use rnix::{
7 types::{LetIn, TypedNode}, 6 types::{LetIn, TypedNode},
8 NodeOrToken, SyntaxElement, SyntaxKind, TextRange 7 NodeOrToken, SyntaxElement, SyntaxKind, TextRange,
9}; 8};
9use 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 @@
1use crate::{Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use macros::lint;
5use rnix::{ 5use 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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)]
16struct RedundantPatternBind; 35struct 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 @@
1use crate::{make, Lint, Metadata, Report, Rule, Suggestion}; 1use crate::{make, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use 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 @@
1use crate::{Lint, Metadata, Report, Rule, Suggestion, Diagnostic}; 1use crate::{Diagnostic, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use macros::lint;
5use rnix::{ 5use 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 @@
1use proc_macro2::TokenStream as TokenStream2;
2use quote::quote;
3use syn::{ItemStruct, Lit, Meta, MetaNameValue};
4
5pub 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 @@
1use std::collections::HashMap; 1mod explain;
2mod metadata;
2 3
4use explain::generate_explain_impl;
5use metadata::{generate_meta_impl, RawLintMeta};
3use proc_macro::TokenStream; 6use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2; 7use proc_macro2::TokenStream as TokenStream2;
5 8use quote::quote;
6use quote::{format_ident, quote}; 9use syn::{parse_macro_input, Ident, ItemStruct};
7use syn::{
8 parse::{Parse, ParseStream, Result as ParseResult},
9 parse_macro_input,
10 punctuated::Punctuated,
11 Expr, Ident, ItemStruct, Lit, Token,
12};
13
14struct KeyValue {
15 key: Ident,
16 _eq: Token![=],
17 value: Expr,
18}
19
20impl 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
30struct LintMeta(HashMap<Ident, Expr>);
31
32impl 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
43fn generate_self_impl(struct_name: &Ident) -> TokenStream2 { 11fn 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
53fn 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
72fn 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
89fn 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) = &note_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
106fn 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
123fn generate_report_fn() -> TokenStream2 {
124 quote! {
125 fn report() -> Report {
126 Report::new(Self::note(), Self::code())
127 }
128 }
129}
130
131fn 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
153fn 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]
176pub fn lint(attr: TokenStream, item: TokenStream) -> TokenStream { 22pub 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 @@
1use std::collections::HashMap;
2
3use proc_macro2::TokenStream as TokenStream2;
4use quote::{format_ident, quote};
5use syn::{
6 parse::{Parse, ParseStream, Result},
7 punctuated::Punctuated,
8 Expr, ExprArray, Ident, Lit, Path, Token,
9};
10
11struct KeyValue {
12 key: Ident,
13 _eq: Token![=],
14 value: Expr,
15}
16
17impl 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
27pub struct RawLintMeta(HashMap<Ident, Expr>);
28
29impl 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
40pub struct LintMeta<'μ> {
41 name: &'μ Lit,
42 note: &'μ Lit,
43 code: &'μ Lit,
44 match_with: MatchWith<'μ>,
45}
46
47enum MatchWith<'π> {
48 Path(&'π Path),
49 Array(&'π ExprArray),
50}
51
52fn extract<'λ>(id: &str, raw: &'λ RawLintMeta) -> &'λ Expr {
53 raw.0
54 .get(&format_ident!("{}", id))
55 .unwrap_or_else(|| panic!("`{}` not present", id))
56}
57
58fn as_lit(e: &Expr) -> &Lit {
59 match e {
60 Expr::Lit(l) => &l.lit,
61 _ => panic!("expected a literal"),
62 }
63}
64
65impl<'μ> 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
158pub 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}