csskit_source_finder/
lib.rs1#![deny(warnings)]
2use std::collections::HashSet;
3use std::io;
4use std::path::PathBuf;
5use std::str::from_utf8;
6
7use glob::glob;
8use grep_matcher::{Captures, Matcher};
9use grep_regex::{RegexMatcher, RegexMatcherBuilder};
10use grep_searcher::{Searcher, SearcherBuilder, Sink, SinkError, SinkMatch};
11use syn::{DeriveInput, parse_str};
12
13pub struct NodeMatcher<'a> {
14 matcher: &'a RegexMatcher,
15 matches: &'a mut HashSet<DeriveInput>,
16}
17
18impl Sink for NodeMatcher<'_> {
19 type Error = io::Error;
20
21 fn matched(&mut self, _searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, io::Error> {
22 let mut captures = self.matcher.new_captures()?;
23 let line = match from_utf8(mat.bytes()) {
24 Ok(matched) => matched,
25 Err(err) => return Err(io::Error::error_message(err)),
26 };
27 self.matcher.captures_iter(mat.bytes(), &mut captures, |captures| -> bool {
28 let capture = format!("{} {} {{}}", &line[captures.get(2).unwrap()], &line[captures.get(5).unwrap()]);
29 match parse_str::<DeriveInput>(&capture) {
30 Ok(ty) => {
31 self.matches.insert(ty);
32 }
33 Err(err) => {
34 panic!("#[visit] or unknown: {capture} {err}");
35 }
36 }
37 true
38 })?;
39 Ok(true)
40 }
41}
42
43pub fn find_visitable_nodes(dir: &str, matches: &mut HashSet<DeriveInput>, path_callback: impl Fn(&PathBuf)) {
44 let matcher = RegexMatcherBuilder::new()
45 .multi_line(true)
46 .dot_matches_new_line(true)
47 .ignore_whitespace(true)
48 .build(
49 r#"
50 ^\s*\#\[
51 # munch `cfg_atr(...,` and optional `derive(...)`.
52 (?:cfg_attr\([^,]+,\s*(?:derive\([^\)]+\),\s*)?)?
53 # match the #[visit] attribute
54 (visit)
55 # munch the data between the attribute and the definition
56 .*?
57 (
58 # Is this a public definition?
59 pub\s*(?:struct|enum)\s*
60 )
61 # munch any comments/attributes between this and our name (for macros)
62 (:?\n?\s*(:?\/\/|\#)[^\n]*)*
63 # finally grab the word (plus any generics)
64 \s*(\w*(:?<[^>]+>)?)"#,
65 )
66 .unwrap();
67 let mut searcher = SearcherBuilder::new().line_number(false).multi_line(true).build();
68 let entries = glob(dir).unwrap();
69 for entry in entries.filter_map(|p| p.ok()) {
70 path_callback(&entry);
71 let context = NodeMatcher { matcher: &matcher, matches };
72 searcher.search_path(&matcher, entry, context).unwrap();
73 }
74}
75
76#[test]
77fn test_find_visitable_nodes() {
78 use itertools::Itertools;
79 use quote::ToTokens;
80 let mut matches = HashSet::new();
81 find_visitable_nodes("../css_ast/src/**/*.rs", &mut matches, |_| {});
82 ::insta::assert_ron_snapshot!(
83 "all_visitable_nodes",
84 matches.iter().map(|ty| ty.to_token_stream().to_string()).sorted().collect::<Vec<_>>()
85 );
86}