aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/src/lib.rs42
-rw-r--r--lib/src/lints/bool_comparison.rs88
-rw-r--r--lib/src/make.rs20
3 files changed, 110 insertions, 40 deletions
diff --git a/lib/src/lib.rs b/lib/src/lib.rs
index c06f02d..0f92d0d 100644
--- a/lib/src/lib.rs
+++ b/lib/src/lib.rs
@@ -1,8 +1,10 @@
1mod lints; 1mod lints;
2mod make;
3
2pub use lints::LINTS; 4pub use lints::LINTS;
3 5
4use rnix::{SyntaxElement, SyntaxKind, TextRange}; 6use rnix::{SyntaxElement, SyntaxKind, TextRange};
5use std::default::Default; 7use std::{default::Default, convert::Into};
6 8
7pub trait Rule { 9pub trait Rule {
8 fn validate(&self, node: &SyntaxElement) -> Option<Report>; 10 fn validate(&self, node: &SyntaxElement) -> Option<Report>;
@@ -12,32 +14,60 @@ pub trait Rule {
12pub struct Diagnostic { 14pub struct Diagnostic {
13 pub at: TextRange, 15 pub at: TextRange,
14 pub message: String, 16 pub message: String,
17 pub suggestion: Option<Suggestion>,
15} 18}
16 19
17impl Diagnostic { 20impl Diagnostic {
18 pub fn new(at: TextRange, message: String) -> Self { 21 pub fn new(at: TextRange, message: String) -> Self {
19 Self { at, message } 22 Self { at, message, suggestion: None }
23 }
24 pub fn suggest(at: TextRange, message: String, suggestion: Suggestion) -> Self {
25 Self { at, message, suggestion: Some(suggestion) }
26 }
27}
28
29#[derive(Debug)]
30pub struct Suggestion {
31 pub at: TextRange,
32 pub fix: SyntaxElement,
33}
34
35impl Suggestion {
36 pub fn new<E: Into<SyntaxElement>>(at: TextRange, fix: E) -> Self {
37 Self {
38 at,
39 fix: fix.into()
40 }
20 } 41 }
21} 42}
22 43
23#[derive(Debug, Default)] 44#[derive(Debug, Default)]
24pub struct Report { 45pub struct Report {
25 pub diagnostics: Vec<Diagnostic>, 46 pub diagnostics: Vec<Diagnostic>,
47 pub note: &'static str
26} 48}
27 49
28impl Report { 50impl Report {
29 pub fn new() -> Self { 51 pub fn new(note: &'static str) -> Self {
30 Self::default() 52 Self {
53 note,
54 ..Default::default()
55 }
31 } 56 }
32 pub fn diagnostic(mut self, at: TextRange, message: String) -> Self { 57 pub fn diagnostic(mut self, at: TextRange, message: String) -> Self {
33 self.diagnostics.push(Diagnostic::new(at, message)); 58 self.diagnostics.push(Diagnostic::new(at, message));
34 self 59 self
35 } 60 }
61 pub fn suggest(mut self, at: TextRange, message: String, suggestion: Suggestion) -> Self {
62 self.diagnostics.push(Diagnostic::suggest(at, message, suggestion));
63 self
64 }
65
36} 66}
37 67
38pub trait Metadata { 68pub trait Metadata {
39 fn name(&self) -> &str; 69 fn name() -> &'static str where Self: Sized;
40 fn note(&self) -> &str; 70 fn note() -> &'static str where Self: Sized;
41 fn match_with(&self, with: &SyntaxKind) -> bool; 71 fn match_with(&self, with: &SyntaxKind) -> bool;
42 fn match_kind(&self) -> SyntaxKind; 72 fn match_kind(&self) -> SyntaxKind;
43} 73}
diff --git a/lib/src/lints/bool_comparison.rs b/lib/src/lints/bool_comparison.rs
index 918126f..f869e17 100644
--- a/lib/src/lints/bool_comparison.rs
+++ b/lib/src/lints/bool_comparison.rs
@@ -1,4 +1,4 @@
1use crate::{Lint, Metadata, Report, Rule}; 1use crate::{make, Lint, Metadata, Report, Rule, Suggestion};
2 2
3use if_chain::if_chain; 3use if_chain::if_chain;
4use macros::lint; 4use macros::lint;
@@ -22,22 +22,58 @@ impl Rule for BoolComparison {
22 if let Some(lhs) = bin_expr.lhs(); 22 if let Some(lhs) = bin_expr.lhs();
23 if let Some(rhs) = bin_expr.rhs(); 23 if let Some(rhs) = bin_expr.rhs();
24 24
25 if let BinOpKind::Equal | BinOpKind::NotEqual = bin_expr.operator(); 25 if let op@(BinOpKind::Equal | BinOpKind::NotEqual) = bin_expr.operator();
26 let (non_bool_side, bool_side) = if is_boolean_ident(&lhs) { 26 let (non_bool_side, bool_side) = if boolean_ident(&lhs).is_some() {
27 (rhs, lhs) 27 (rhs, lhs)
28 } else if is_boolean_ident(&rhs) { 28 } else if boolean_ident(&rhs).is_some() {
29 (lhs, rhs) 29 (lhs, rhs)
30 } else { 30 } else {
31 return None 31 return None
32 }; 32 };
33 then { 33 then {
34 let at = node.text_range(); 34 let at = node.text_range();
35 let replacement = {
36 match (boolean_ident(&bool_side).unwrap(), op == BinOpKind::Equal) {
37 (NixBoolean::True, true) | (NixBoolean::False, false) => {
38 // `a == true`, `a != false` replace with just `a`
39 non_bool_side.clone()
40 },
41 (NixBoolean::True, false) | (NixBoolean::False, true) => {
42 // `a != true`, `a == false` replace with `!a`
43 match non_bool_side.kind() {
44 SyntaxKind::NODE_APPLY
45 | SyntaxKind::NODE_PAREN
46 | SyntaxKind::NODE_IDENT => {
47 // do not parenthsize the replacement
48 make::unary_not(&non_bool_side).node().clone()
49 },
50 SyntaxKind::NODE_BIN_OP => {
51 let inner = BinOp::cast(non_bool_side.clone()).unwrap();
52 // `!a ? b`, no paren required
53 if inner.operator() == BinOpKind::IsSet {
54 make::unary_not(&non_bool_side).node().clone()
55 } else {
56 let parens = make::parenthesize(&non_bool_side);
57 make::unary_not(parens.node()).node().clone()
58 }
59 },
60 _ => {
61 let parens = make::parenthesize(&non_bool_side);
62 make::unary_not(parens.node()).node().clone()
63 }
64 }
65 },
66 }
67 };
35 let message = format!( 68 let message = format!(
36 "Comparing `{}` with boolean literal `{}`", 69 "Comparing `{}` with boolean literal `{}`",
37 non_bool_side, 70 non_bool_side,
38 bool_side 71 bool_side
39 ); 72 );
40 Some(Report::new().diagnostic(at, message)) 73 Some(
74 Report::new(Self::note())
75 .suggest(at, message, Suggestion::new(at, replacement))
76 )
41 } else { 77 } else {
42 None 78 None
43 } 79 }
@@ -45,34 +81,18 @@ impl Rule for BoolComparison {
45 } 81 }
46} 82}
47 83
48// not entirely accurate, underhanded nix programmers might write `true = false` 84enum NixBoolean {
49fn is_boolean_ident(node: &SyntaxNode) -> bool { 85 True,
50 if let Some(ident_expr) = Ident::cast(node.clone()) { 86 False,
51 ident_expr.as_str() == "true" || ident_expr.as_str() == "false"
52 } else {
53 false
54 }
55} 87}
56 88
57// #[cfg(test)] 89// not entirely accurate, underhanded nix programmers might write `true = false`
58// mod tests { 90fn boolean_ident(node: &SyntaxNode) -> Option<NixBoolean> {
59// use super::*; 91 Ident::cast(node.clone())
60// use rnix::{parser, WalkEvent}; 92 .map(|ident_expr| match ident_expr.as_str() {
61// 93 "true" => Some(NixBoolean::True),
62// #[test] 94 "false" => Some(NixBoolean::False),
63// fn trivial() { 95 _ => None,
64// let src = r#" 96 })
65// a == true 97 .flatten()
66// "#; 98}
67// let parsed = rnix::parse(src).as_result().ok().unwrap();
68// let _ = parsed
69// .node()
70// .preorder_with_tokens()
71// .filter_map(|event| match event {
72// WalkEvent::Enter(t) => Some(t),
73// _ => None,
74// })
75// .map(|node| BoolComparison.validate(&node))
76// .collect::<Vec<_>>();
77// }
78// }
diff --git a/lib/src/make.rs b/lib/src/make.rs
new file mode 100644
index 0000000..0b21105
--- /dev/null
+++ b/lib/src/make.rs
@@ -0,0 +1,20 @@
1use rnix::{SyntaxNode, types::{TypedNode, self}};
2
3fn ast_from_text<N: TypedNode>(text: &str) -> N {
4 let parse = rnix::parse(text);
5 let node = match parse.node().descendants().find_map(N::cast) {
6 Some(it) => it,
7 None => {
8 panic!("Failed to make ast node `{}` from text {}", std::any::type_name::<N>(), text)
9 }
10 };
11 node
12}
13
14pub fn parenthesize(node: &SyntaxNode) -> types::Paren {
15 ast_from_text(&format!("({})", node))
16}
17
18pub fn unary_not(node: &SyntaxNode) -> types::UnaryOp {
19 ast_from_text(&format!("!{}", node))
20}