From cdbfef69a8039218eedb7a119c6764cb6d8f2179 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 12:57:17 -0400 Subject: [PATCH 1/8] working on rules --- Cargo.toml | 8 +++++ src/args.rs | 4 +++ src/bin/tree-sitter-lint.rs | 7 ++++ src/context.rs | 17 ++++++++++ src/lib.rs | 65 +++++++++++++++++++++++++++++++------ src/rule.rs | 59 +++++++++++++++++++++++++++++++++ src/violation.rs | 9 +++++ 7 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/args.rs create mode 100644 src/bin/tree-sitter-lint.rs create mode 100644 src/context.rs create mode 100644 src/rule.rs create mode 100644 src/violation.rs diff --git a/Cargo.toml b/Cargo.toml index a5eea2a..455a61f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = "4.3.17" +derive_builder = "0.12.0" +tree-sitter = "0.20.10" +tree-sitter-grep = { git = "https://github.com/helixbass/tree-sitter-grep", rev = "6983baa6" } +tree-sitter-rust = "0.20.3" + +[[bin]] +name = "tree-sitter-lint" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..41acdde --- /dev/null +++ b/src/args.rs @@ -0,0 +1,4 @@ +use clap::Parser; + +#[derive(Parser)] +pub struct Args {} diff --git a/src/bin/tree-sitter-lint.rs b/src/bin/tree-sitter-lint.rs new file mode 100644 index 0000000..030f32f --- /dev/null +++ b/src/bin/tree-sitter-lint.rs @@ -0,0 +1,7 @@ +use clap::Parser; +use tree_sitter_lint::{run, Args}; + +fn main() { + let args = Args::parse(); + run(args); +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..c22e966 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,17 @@ +use tree_sitter::Language; + +use crate::violation::Violation; + +pub struct Context { + language: Language, +} + +impl Context { + pub fn new(language: Language) -> Self { + Self { language } + } + + pub fn report(&self, violation: Violation) { + unimplemented!() + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..0cb919b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,59 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +mod args; +mod context; +mod rule; +mod violation; + +pub use args::Args; +use rule::{Rule, RuleBuilder, RuleListenerBuilder}; +use violation::ViolationBuilder; + +use crate::context::Context; + +pub fn run(args: Args) { + let language = tree_sitter_rust::language(); + let context = Context::new(language); + let resolved_rules = get_rules() + .into_iter() + .map(|rule| rule.resolve(&context)) + .collect::>(); + unimplemented!() } -#[cfg(test)] -mod tests { - use super::*; +fn get_rules() -> Vec { + vec![no_default_default_rule()] +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +fn no_default_default_rule() -> Rule { + RuleBuilder::default() + .name("no_default_default") + .create(|context| { + vec![RuleListenerBuilder::default() + .query( + r#"( + (call_expression + function: + (scoped_identifier + path: + (identifier) @first (#eq? @first "Default") + name: + (identifier) @second (#eq? @second "default") + ) + ) @c + )"#, + ) + .capture_name("c") + .on_query_match(|node| { + context.report( + ViolationBuilder::default() + .message(r#"Use '_d()' instead of 'Default::default()'"#) + .node(node) + .build() + .unwrap(), + ); + }) + .build() + .unwrap()] + }) + .build() + .unwrap() } diff --git a/src/rule.rs b/src/rule.rs new file mode 100644 index 0000000..79e7961 --- /dev/null +++ b/src/rule.rs @@ -0,0 +1,59 @@ +use std::rc::Rc; + +use derive_builder::Builder; +use tree_sitter::Node; + +use crate::context::Context; + +#[derive(Builder)] +#[builder(setter(into))] +pub struct Rule { + pub name: String, + #[builder(setter(custom))] + pub create: Rc Vec>, +} + +impl Rule { + pub fn resolve(self, context: &Context) -> ResolvedRule<'_> { + let Rule { name, create } = self; + + ResolvedRule::new(name, create(context)) + } +} + +impl RuleBuilder { + pub fn create( + &mut self, + callback: impl Fn(&Context) -> Vec + 'static, + ) -> &mut Self { + self.create = Some(Rc::new(callback)); + self + } +} + +pub struct ResolvedRule<'context> { + pub name: String, + pub listeners: Vec>, +} + +impl<'context> ResolvedRule<'context> { + pub fn new(name: String, listeners: Vec>) -> Self { + Self { name, listeners } + } +} + +#[derive(Builder)] +#[builder(setter(into, strip_option))] +pub struct RuleListener<'on_query_match> { + pub query: String, + pub capture_name: Option, + #[builder(setter(custom))] + pub on_query_match: Rc, +} + +impl<'on_query_match> RuleListenerBuilder<'on_query_match> { + pub fn on_query_match(&mut self, callback: impl Fn(&Node) + 'on_query_match) -> &mut Self { + self.on_query_match = Some(Rc::new(callback)); + self + } +} diff --git a/src/violation.rs b/src/violation.rs new file mode 100644 index 0000000..f11d9b9 --- /dev/null +++ b/src/violation.rs @@ -0,0 +1,9 @@ +use derive_builder::Builder; +use tree_sitter::Node; + +#[derive(Builder)] +#[builder(setter(into))] +pub struct Violation<'node> { + pub message: String, + pub node: &'node Node<'node>, +} From 3cb0b0fd3c9ae630b59d34808d4ab1c0e1cabdf5 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 13:07:21 -0400 Subject: [PATCH 2/8] resolve queries --- src/context.rs | 2 +- src/rule.rs | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/context.rs b/src/context.rs index c22e966..3d1bdf9 100644 --- a/src/context.rs +++ b/src/context.rs @@ -3,7 +3,7 @@ use tree_sitter::Language; use crate::violation::Violation; pub struct Context { - language: Language, + pub language: Language, } impl Context { diff --git a/src/rule.rs b/src/rule.rs index 79e7961..bb0d685 100644 --- a/src/rule.rs +++ b/src/rule.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use derive_builder::Builder; -use tree_sitter::Node; +use tree_sitter::{Node, Query}; use crate::context::Context; @@ -17,7 +17,13 @@ impl Rule { pub fn resolve(self, context: &Context) -> ResolvedRule<'_> { let Rule { name, create } = self; - ResolvedRule::new(name, create(context)) + ResolvedRule::new( + name, + create(context) + .into_iter() + .map(|rule_listener| rule_listener.resolve(context)) + .collect(), + ) } } @@ -33,11 +39,11 @@ impl RuleBuilder { pub struct ResolvedRule<'context> { pub name: String, - pub listeners: Vec>, + pub listeners: Vec>, } impl<'context> ResolvedRule<'context> { - pub fn new(name: String, listeners: Vec>) -> Self { + pub fn new(name: String, listeners: Vec>) -> Self { Self { name, listeners } } } @@ -51,9 +57,38 @@ pub struct RuleListener<'on_query_match> { pub on_query_match: Rc, } +impl<'on_query_match> RuleListener<'on_query_match> { + pub fn resolve(self, context: &Context) -> ResolvedRuleListener<'on_query_match> { + let RuleListener { + query, + capture_name, + on_query_match, + } = self; + let query = Query::new(context.language, &query).unwrap(); + let capture_index = match capture_name { + None => match query.capture_names().len() { + 0 => panic!("Expected capture"), + _ => 0, + }, + Some(capture_name) => query.capture_index_for_name(&capture_name).unwrap(), + }; + ResolvedRuleListener { + query, + capture_index, + on_query_match, + } + } +} + impl<'on_query_match> RuleListenerBuilder<'on_query_match> { pub fn on_query_match(&mut self, callback: impl Fn(&Node) + 'on_query_match) -> &mut Self { self.on_query_match = Some(Rc::new(callback)); self } } + +pub struct ResolvedRuleListener<'on_query_match> { + pub query: Query, + pub capture_index: u32, + pub on_query_match: Rc, +} From e4fc8a313f1ddb473e9a7c7f267658fab5e5733c Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 13:25:07 -0400 Subject: [PATCH 3/8] aggregate queries --- src/lib.rs | 35 +++++++++++++++++++++++++++++++++-- src/rule.rs | 6 ++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0cb919b..27e3d04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,8 @@ mod rule; mod violation; pub use args::Args; -use rule::{Rule, RuleBuilder, RuleListenerBuilder}; +use rule::{ResolvedRule, Rule, RuleBuilder, RuleListenerBuilder}; +use tree_sitter::Query; use violation::ViolationBuilder; use crate::context::Context; @@ -16,7 +17,37 @@ pub fn run(args: Args) { .into_iter() .map(|rule| rule.resolve(&context)) .collect::>(); - unimplemented!() + let aggregated_queries = AggregatedQueries::new(&resolved_rules, &context); +} + +type RuleIndex = usize; +type RuleListenerIndex = usize; + +struct AggregatedQueries { + pattern_index_lookup: Vec<(RuleIndex, RuleListenerIndex)>, + query: Query, +} + +impl AggregatedQueries { + pub fn new(resolved_rules: &[ResolvedRule], context: &Context) -> Self { + let mut pattern_index_lookup: Vec<(RuleIndex, RuleListenerIndex)> = Default::default(); + let mut aggregated_query_text = String::new(); + for (rule_index, resolved_rule) in resolved_rules.into_iter().enumerate() { + for (rule_listener_index, rule_listener) in resolved_rule.listeners.iter().enumerate() { + for _ in 0..rule_listener.query.pattern_count() { + pattern_index_lookup.push((rule_index, rule_listener_index)); + } + aggregated_query_text.push_str(&rule_listener.query_text); + aggregated_query_text.push_str("\n\n"); + } + } + let query = Query::new(context.language, &aggregated_query_text).unwrap(); + assert!(query.pattern_count() == pattern_index_lookup.len()); + Self { + pattern_index_lookup, + query, + } + } } fn get_rules() -> Vec { diff --git a/src/rule.rs b/src/rule.rs index bb0d685..3ad9d5d 100644 --- a/src/rule.rs +++ b/src/rule.rs @@ -60,11 +60,11 @@ pub struct RuleListener<'on_query_match> { impl<'on_query_match> RuleListener<'on_query_match> { pub fn resolve(self, context: &Context) -> ResolvedRuleListener<'on_query_match> { let RuleListener { - query, + query: query_text, capture_name, on_query_match, } = self; - let query = Query::new(context.language, &query).unwrap(); + let query = Query::new(context.language, &query_text).unwrap(); let capture_index = match capture_name { None => match query.capture_names().len() { 0 => panic!("Expected capture"), @@ -74,6 +74,7 @@ impl<'on_query_match> RuleListener<'on_query_match> { }; ResolvedRuleListener { query, + query_text, capture_index, on_query_match, } @@ -89,6 +90,7 @@ impl<'on_query_match> RuleListenerBuilder<'on_query_match> { pub struct ResolvedRuleListener<'on_query_match> { pub query: Query, + pub query_text: String, pub capture_index: u32, pub on_query_match: Rc, } From c2dfc80d9bfe7c714e2678a65fe1f21a574fea9e Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 14:22:36 -0400 Subject: [PATCH 4/8] print violations --- Cargo.toml | 3 ++- src/context.rs | 24 +++++++++++++++++++++++- src/lib.rs | 26 ++++++++++++++++++++++++-- src/rule.rs | 19 +++++++++++-------- src/violation.rs | 7 +++++-- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 455a61f..dbc01d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ edition = "2021" clap = "4.3.17" derive_builder = "0.12.0" tree-sitter = "0.20.10" -tree-sitter-grep = { git = "https://github.com/helixbass/tree-sitter-grep", rev = "6983baa6" } +# tree-sitter-grep = { git = "https://github.com/helixbass/tree-sitter-grep", rev = "6983baa6" } +tree-sitter-grep = { path = "../tree-sitter-grep" } tree-sitter-rust = "0.20.3" [[bin]] diff --git a/src/context.rs b/src/context.rs index 3d1bdf9..ac08c2f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use tree_sitter::Language; use crate::violation::Violation; @@ -12,6 +14,26 @@ impl Context { } pub fn report(&self, violation: Violation) { - unimplemented!() + print_violation(&violation); + } +} + +fn print_violation(violation: &Violation) { + eprintln!( + "{:?}:{}:{} {}", + violation.query_match_context.path, + violation.node.range().start_point.row + 1, + violation.node.range().start_point.column + 1, + violation.message + ); +} + +pub struct QueryMatchContext<'path> { + pub path: &'path Path, +} + +impl<'path> QueryMatchContext<'path> { + pub fn new(path: &'path Path) -> Self { + Self { path } } } diff --git a/src/lib.rs b/src/lib.rs index 27e3d04..1bd0c57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,13 +4,15 @@ mod rule; mod violation; pub use args::Args; +use clap::Parser; +use context::QueryMatchContext; use rule::{ResolvedRule, Rule, RuleBuilder, RuleListenerBuilder}; use tree_sitter::Query; use violation::ViolationBuilder; use crate::context::Context; -pub fn run(args: Args) { +pub fn run(_args: Args) { let language = tree_sitter_rust::language(); let context = Context::new(language); let resolved_rules = get_rules() @@ -18,6 +20,23 @@ pub fn run(args: Args) { .map(|rule| rule.resolve(&context)) .collect::>(); let aggregated_queries = AggregatedQueries::new(&resolved_rules, &context); + let tree_sitter_grep_args = tree_sitter_grep::Args::parse_from([ + "tree_sitter_grep", + "-q", + &aggregated_queries.query_text, + "-l", + "rust", + ]); + tree_sitter_grep::run_with_callback( + tree_sitter_grep_args, + |capture_info, file_contents, path| { + let (rule_index, rule_listener_index) = + aggregated_queries.pattern_index_lookup[capture_info.pattern_index]; + let listener = &resolved_rules[rule_index].listeners[rule_listener_index]; + (listener.on_query_match)(&capture_info.node, &QueryMatchContext::new(path)); + }, + ) + .unwrap(); } type RuleIndex = usize; @@ -26,6 +45,7 @@ type RuleListenerIndex = usize; struct AggregatedQueries { pattern_index_lookup: Vec<(RuleIndex, RuleListenerIndex)>, query: Query, + query_text: String, } impl AggregatedQueries { @@ -46,6 +66,7 @@ impl AggregatedQueries { Self { pattern_index_lookup, query, + query_text: aggregated_query_text, } } } @@ -73,11 +94,12 @@ fn no_default_default_rule() -> Rule { )"#, ) .capture_name("c") - .on_query_match(|node| { + .on_query_match(|node, query_match_context| { context.report( ViolationBuilder::default() .message(r#"Use '_d()' instead of 'Default::default()'"#) .node(node) + .query_match_context(query_match_context) .build() .unwrap(), ); diff --git a/src/rule.rs b/src/rule.rs index 3ad9d5d..afe90b3 100644 --- a/src/rule.rs +++ b/src/rule.rs @@ -1,16 +1,16 @@ -use std::rc::Rc; +use std::sync::Arc; use derive_builder::Builder; use tree_sitter::{Node, Query}; -use crate::context::Context; +use crate::context::{Context, QueryMatchContext}; #[derive(Builder)] #[builder(setter(into))] pub struct Rule { pub name: String, #[builder(setter(custom))] - pub create: Rc Vec>, + pub create: Arc Vec>, } impl Rule { @@ -32,7 +32,7 @@ impl RuleBuilder { &mut self, callback: impl Fn(&Context) -> Vec + 'static, ) -> &mut Self { - self.create = Some(Rc::new(callback)); + self.create = Some(Arc::new(callback)); self } } @@ -54,7 +54,7 @@ pub struct RuleListener<'on_query_match> { pub query: String, pub capture_name: Option, #[builder(setter(custom))] - pub on_query_match: Rc, + pub on_query_match: Arc, } impl<'on_query_match> RuleListener<'on_query_match> { @@ -82,8 +82,11 @@ impl<'on_query_match> RuleListener<'on_query_match> { } impl<'on_query_match> RuleListenerBuilder<'on_query_match> { - pub fn on_query_match(&mut self, callback: impl Fn(&Node) + 'on_query_match) -> &mut Self { - self.on_query_match = Some(Rc::new(callback)); + pub fn on_query_match( + &mut self, + callback: impl Fn(&Node, &QueryMatchContext) + 'on_query_match + Send + Sync, + ) -> &mut Self { + self.on_query_match = Some(Arc::new(callback)); self } } @@ -92,5 +95,5 @@ pub struct ResolvedRuleListener<'on_query_match> { pub query: Query, pub query_text: String, pub capture_index: u32, - pub on_query_match: Rc, + pub on_query_match: Arc, } diff --git a/src/violation.rs b/src/violation.rs index f11d9b9..1c206e9 100644 --- a/src/violation.rs +++ b/src/violation.rs @@ -1,9 +1,12 @@ use derive_builder::Builder; use tree_sitter::Node; +use crate::context::QueryMatchContext; + #[derive(Builder)] #[builder(setter(into))] -pub struct Violation<'node> { +pub struct Violation<'a> { pub message: String, - pub node: &'node Node<'node>, + pub node: &'a Node<'a>, + pub query_match_context: &'a QueryMatchContext<'a>, } From 913e646ad0c40eea432ba1de1b3d0232e1fb650b Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 14:29:20 -0400 Subject: [PATCH 5/8] rule name --- src/context.rs | 39 +++++++++++++++++++++++---------------- src/lib.rs | 14 +++++++++----- src/violation.rs | 3 --- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/context.rs b/src/context.rs index ac08c2f..2d11999 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,7 +2,7 @@ use std::path::Path; use tree_sitter::Language; -use crate::violation::Violation; +use crate::{rule::ResolvedRule, violation::Violation}; pub struct Context { pub language: Language, @@ -12,28 +12,35 @@ impl Context { pub fn new(language: Language) -> Self { Self { language } } +} + +pub struct QueryMatchContext<'a> { + pub path: &'a Path, + pub file_contents: &'a [u8], + pub rule: &'a ResolvedRule<'a>, +} + +impl<'a> QueryMatchContext<'a> { + pub fn new(path: &'a Path, file_contents: &'a [u8], rule: &'a ResolvedRule) -> Self { + Self { + path, + file_contents, + rule, + } + } pub fn report(&self, violation: Violation) { - print_violation(&violation); + print_violation(&violation, self); } } -fn print_violation(violation: &Violation) { +fn print_violation(violation: &Violation, query_match_context: &QueryMatchContext) { eprintln!( - "{:?}:{}:{} {}", - violation.query_match_context.path, + "{:?}:{}:{} {} {}", + query_match_context.path, violation.node.range().start_point.row + 1, violation.node.range().start_point.column + 1, - violation.message + violation.message, + query_match_context.rule.name, ); } - -pub struct QueryMatchContext<'path> { - pub path: &'path Path, -} - -impl<'path> QueryMatchContext<'path> { - pub fn new(path: &'path Path) -> Self { - Self { path } - } -} diff --git a/src/lib.rs b/src/lib.rs index 1bd0c57..938667d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,8 +32,12 @@ pub fn run(_args: Args) { |capture_info, file_contents, path| { let (rule_index, rule_listener_index) = aggregated_queries.pattern_index_lookup[capture_info.pattern_index]; - let listener = &resolved_rules[rule_index].listeners[rule_listener_index]; - (listener.on_query_match)(&capture_info.node, &QueryMatchContext::new(path)); + let rule = &resolved_rules[rule_index]; + let listener = &rule.listeners[rule_listener_index]; + (listener.on_query_match)( + &capture_info.node, + &QueryMatchContext::new(path, file_contents, rule), + ); }, ) .unwrap(); @@ -44,6 +48,7 @@ type RuleListenerIndex = usize; struct AggregatedQueries { pattern_index_lookup: Vec<(RuleIndex, RuleListenerIndex)>, + #[allow(dead_code)] query: Query, query_text: String, } @@ -78,7 +83,7 @@ fn get_rules() -> Vec { fn no_default_default_rule() -> Rule { RuleBuilder::default() .name("no_default_default") - .create(|context| { + .create(|_context| { vec![RuleListenerBuilder::default() .query( r#"( @@ -95,11 +100,10 @@ fn no_default_default_rule() -> Rule { ) .capture_name("c") .on_query_match(|node, query_match_context| { - context.report( + query_match_context.report( ViolationBuilder::default() .message(r#"Use '_d()' instead of 'Default::default()'"#) .node(node) - .query_match_context(query_match_context) .build() .unwrap(), ); diff --git a/src/violation.rs b/src/violation.rs index 1c206e9..0627a7f 100644 --- a/src/violation.rs +++ b/src/violation.rs @@ -1,12 +1,9 @@ use derive_builder::Builder; use tree_sitter::Node; -use crate::context::QueryMatchContext; - #[derive(Builder)] #[builder(setter(into))] pub struct Violation<'a> { pub message: String, pub node: &'a Node<'a>, - pub query_match_context: &'a QueryMatchContext<'a>, } From c36fcfcd2b816704c5f7fb9cc0a85eb8342f22c0 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 14:34:43 -0400 Subject: [PATCH 6/8] print to stdout --- src/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/context.rs b/src/context.rs index 2d11999..f4ef9cb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -35,7 +35,7 @@ impl<'a> QueryMatchContext<'a> { } fn print_violation(violation: &Violation, query_match_context: &QueryMatchContext) { - eprintln!( + println!( "{:?}:{}:{} {} {}", query_match_context.path, violation.node.range().start_point.row + 1, From 5ac45469017a4cbb5b41526397d86c3be18a28f5 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 14:38:32 -0400 Subject: [PATCH 7/8] exit code --- src/context.rs | 15 +++++++++++++-- src/lib.rs | 13 ++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/context.rs b/src/context.rs index f4ef9cb..32a0e0e 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,7 @@ -use std::path::Path; +use std::{ + path::Path, + sync::atomic::{AtomicBool, Ordering}, +}; use tree_sitter::Language; @@ -18,18 +21,26 @@ pub struct QueryMatchContext<'a> { pub path: &'a Path, pub file_contents: &'a [u8], pub rule: &'a ResolvedRule<'a>, + reported_any_violations: &'a AtomicBool, } impl<'a> QueryMatchContext<'a> { - pub fn new(path: &'a Path, file_contents: &'a [u8], rule: &'a ResolvedRule) -> Self { + pub fn new( + path: &'a Path, + file_contents: &'a [u8], + rule: &'a ResolvedRule, + reported_any_violations: &'a AtomicBool, + ) -> Self { Self { path, file_contents, rule, + reported_any_violations, } } pub fn report(&self, violation: Violation) { + self.reported_any_violations.store(true, Ordering::Relaxed); print_violation(&violation, self); } } diff --git a/src/lib.rs b/src/lib.rs index 938667d..39f1f30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,11 @@ mod context; mod rule; mod violation; +use std::{ + process, + sync::atomic::{AtomicBool, Ordering}, +}; + pub use args::Args; use clap::Parser; use context::QueryMatchContext; @@ -27,6 +32,7 @@ pub fn run(_args: Args) { "-l", "rust", ]); + let reported_any_violations = AtomicBool::new(false); tree_sitter_grep::run_with_callback( tree_sitter_grep_args, |capture_info, file_contents, path| { @@ -36,11 +42,16 @@ pub fn run(_args: Args) { let listener = &rule.listeners[rule_listener_index]; (listener.on_query_match)( &capture_info.node, - &QueryMatchContext::new(path, file_contents, rule), + &QueryMatchContext::new(path, file_contents, rule, &reported_any_violations), ); }, ) .unwrap(); + if reported_any_violations.load(Ordering::Relaxed) { + process::exit(1); + } else { + process::exit(0); + } } type RuleIndex = usize; From 1b3c8da8eb57c1c80699b9c17f0ec76998766730 Mon Sep 17 00:00:00 2001 From: Julian Rosse Date: Thu, 20 Jul 2023 15:55:37 -0400 Subject: [PATCH 8/8] second rule --- Cargo.toml | 4 +-- src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++--------- src/rule.rs | 1 + 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbc01d7..3b7c730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,9 @@ edition = "2021" [dependencies] clap = "4.3.17" derive_builder = "0.12.0" +regex = "1.9.1" tree-sitter = "0.20.10" -# tree-sitter-grep = { git = "https://github.com/helixbass/tree-sitter-grep", rev = "6983baa6" } -tree-sitter-grep = { path = "../tree-sitter-grep" } +tree-sitter-grep = { git = "https://github.com/helixbass/tree-sitter-grep", rev = "3d4682c" } tree-sitter-rust = "0.20.3" [[bin]] diff --git a/src/lib.rs b/src/lib.rs index 39f1f30..f68228b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod rule; mod violation; use std::{ + borrow::Cow, process, sync::atomic::{AtomicBool, Ordering}, }; @@ -17,6 +18,17 @@ use violation::ViolationBuilder; use crate::context::Context; +#[macro_export] +macro_rules! regex { + ($re:expr $(,)?) => {{ + static RE: std::sync::OnceLock = std::sync::OnceLock::new(); + RE.get_or_init(|| regex::Regex::new($re).unwrap()) + }}; +} + +const CAPTURE_NAME_FOR_TREE_SITTER_GREP: &str = "_tree_sitter_lint_capture"; +const CAPTURE_NAME_FOR_TREE_SITTER_GREP_WITH_LEADING_AT: &str = "@_tree_sitter_lint_capture"; + pub fn run(_args: Args) { let language = tree_sitter_rust::language(); let context = Context::new(language); @@ -31,6 +43,8 @@ pub fn run(_args: Args) { &aggregated_queries.query_text, "-l", "rust", + "--capture", + CAPTURE_NAME_FOR_TREE_SITTER_GREP, ]); let reported_any_violations = AtomicBool::new(false); tree_sitter_grep::run_with_callback( @@ -73,7 +87,18 @@ impl AggregatedQueries { for _ in 0..rule_listener.query.pattern_count() { pattern_index_lookup.push((rule_index, rule_listener_index)); } - aggregated_query_text.push_str(&rule_listener.query_text); + let use_capture_name = + &rule_listener.query.capture_names()[rule_listener.capture_index as usize]; + let query_text_with_unified_capture_name = + regex!(&format!(r#"@{use_capture_name}\b"#)).replace_all( + &rule_listener.query_text, + CAPTURE_NAME_FOR_TREE_SITTER_GREP_WITH_LEADING_AT, + ); + assert!( + matches!(query_text_with_unified_capture_name, Cow::Owned(_),), + "Didn't find any instances of the capture name to replace" + ); + aggregated_query_text.push_str(&query_text_with_unified_capture_name); aggregated_query_text.push_str("\n\n"); } } @@ -88,7 +113,7 @@ impl AggregatedQueries { } fn get_rules() -> Vec { - vec![no_default_default_rule()] + vec![no_default_default_rule(), no_lazy_static_rule()] } fn no_default_default_rule() -> Rule { @@ -98,16 +123,16 @@ fn no_default_default_rule() -> Rule { vec![RuleListenerBuilder::default() .query( r#"( - (call_expression - function: - (scoped_identifier - path: - (identifier) @first (#eq? @first "Default") - name: - (identifier) @second (#eq? @second "default") - ) - ) @c - )"#, + (call_expression + function: + (scoped_identifier + path: + (identifier) @first (#eq? @first "Default") + name: + (identifier) @second (#eq? @second "default") + ) + ) @c + )"#, ) .capture_name("c") .on_query_match(|node, query_match_context| { @@ -125,3 +150,31 @@ fn no_default_default_rule() -> Rule { .build() .unwrap() } + +fn no_lazy_static_rule() -> Rule { + RuleBuilder::default() + .name("no_lazy_static") + .create(|_context| { + vec![RuleListenerBuilder::default() + .query( + r#"( + (macro_invocation + macro: (identifier) @c (#eq? @c "lazy_static") + ) + )"#, + ) + .on_query_match(|node, query_match_context| { + query_match_context.report( + ViolationBuilder::default() + .message(r#"Prefer 'OnceCell::*::Lazy' to 'lazy_static!()'"#) + .node(node) + .build() + .unwrap(), + ); + }) + .build() + .unwrap()] + }) + .build() + .unwrap() +} diff --git a/src/rule.rs b/src/rule.rs index afe90b3..dcf5aed 100644 --- a/src/rule.rs +++ b/src/rule.rs @@ -52,6 +52,7 @@ impl<'context> ResolvedRule<'context> { #[builder(setter(into, strip_option))] pub struct RuleListener<'on_query_match> { pub query: String, + #[builder(default)] pub capture_name: Option, #[builder(setter(custom))] pub on_query_match: Arc,