tree_sitter_tests_formatter/
lib.rs

1use std::{cmp::max, fs::read_to_string, fs::File, path::Path, path::PathBuf, vec};
2
3use lazy_static::lazy_static;
4use regex::Regex;
5
6mod s_exp_formatter;
7
8pub use s_exp_formatter::format_s_expr;
9
10/// Format tree-sitter test files in the given directory.
11///
12/// # Arguments
13/// `path` - The directory to search for tree-sitter test files.
14pub fn format_tests_dir(path: &Path) {
15    assert!(path.exists(), "Path does not exist: {:?}", path);
16    assert!(path.is_dir(), "Path is not a directory: {:?}", path);
17    let test_files = walk_tests_dir(path);
18    for test_file in test_files.iter() {
19        test_file.format_file(test_file.path());
20    }
21}
22
23/// Format a tree-sitter test file.
24///
25/// # Arguments
26/// `path` - The path to the tree-sitter test file.
27pub fn format_test_file(path: &Path) {
28    assert!(path.exists(), "Path does not exist: {:?}", path);
29    assert!(path.is_file(), "Path is not a file: {:?}", path);
30    let test_file = TestFile::from_file(path);
31    test_file.format_file(path);
32}
33
34#[derive(Debug, PartialEq, Eq, Default, Clone)]
35struct TestFile {
36    path: PathBuf,
37    fixtures: Vec<Fixture>,
38}
39
40impl TestFile {
41    pub fn from_file(path: &Path) -> Self {
42        enum State {
43            InFixtureName,
44            InFixture,
45            InExpected,
46            None,
47        }
48        lazy_static! {
49            static ref RE_FIXTURE_NAME_SEP: Regex = Regex::new(r"^===").unwrap();
50            static ref RE_FIXTURE_SEP: Regex = Regex::new(r"^---$").unwrap();
51        }
52        let mut state: State = State::None;
53        let mut fixtures = Vec::new();
54        let mut cur = Fixture::default();
55        for line in read_to_string(path).unwrap().lines() {
56            match state {
57                State::None => {
58                    // Looking for fixture name
59                    if RE_FIXTURE_NAME_SEP.is_match(line) {
60                        state = State::InFixtureName;
61                    }
62                }
63                State::InFixtureName => {
64                    // Looking for fixture
65                    if RE_FIXTURE_NAME_SEP.is_match(line) {
66                        state = State::InFixture;
67                    }
68                    // Fixture name line
69                    else {
70                        cur.name = line.to_string();
71                    }
72                }
73                State::InFixture => {
74                    // Looking for expected
75                    if RE_FIXTURE_SEP.is_match(line) {
76                        state = State::InExpected;
77                    }
78                    // Fixture line
79                    else {
80                        cur.input.push_str(line);
81                        cur.input.push('\n');
82                    }
83                }
84                State::InExpected => {
85                    // Looking for next fixture
86                    if RE_FIXTURE_NAME_SEP.is_match(line) {
87                        state = State::InFixtureName;
88                        fixtures.push(cur.clone());
89                        cur = Fixture::default();
90                    } else {
91                        cur.expected.push_str(line);
92                        cur.expected.push('\n');
93                    }
94                }
95            }
96        }
97        fixtures.push(cur);
98
99        Self {
100            path: path.to_path_buf(),
101            fixtures,
102        }
103    }
104
105    pub fn format_file(&self, path: &Path) {
106        use std::io::Write;
107        let formatted = self.format();
108        let mut file = File::create(path).unwrap();
109        write!(file, "{}", formatted).unwrap();
110    }
111
112    pub fn path(&self) -> &Path {
113        &self.path
114    }
115
116    fn format(&self) -> String {
117        let mut res = Vec::new();
118        for fixture in self.fixtures.iter() {
119            res.push(fixture.format());
120        }
121
122        res.join("\n".repeat(2).as_str())
123    }
124}
125
126#[derive(Debug, PartialEq, Eq, Default, Clone)]
127struct Fixture {
128    name: String,
129    input: String,
130    expected: String,
131}
132
133impl Fixture {
134    pub fn format(&self) -> String {
135        let res = vec![
136            self.format_name(),
137            "\n\n".to_string(),
138            self.format_input(),
139            "\n\n".to_string(),
140            "---".to_string(),
141            "\n\n".to_string(),
142            self.format_expected(),
143        ];
144
145        res.join("")
146    }
147
148    fn format_name(&self) -> String {
149        let mut res = Vec::new();
150        let n = max(3, self.name.trim().len());
151        let sep = "=".to_string().repeat(n);
152        res.push(sep.clone());
153        res.push(self.name.trim().to_string());
154        res.push(sep.clone());
155
156        res.join("\n")
157    }
158
159    fn format_input(&self) -> String {
160        self.input.trim().to_string()
161    }
162
163    fn format_expected(&self) -> String {
164        s_exp_formatter::format_s_expr(&self.expected)
165    }
166}
167
168fn walk_tests_dir(path: &Path) -> Vec<TestFile> {
169    let mut res: Vec<TestFile> = Vec::new();
170    for entry in path.read_dir().unwrap() {
171        let entry = entry.unwrap();
172        let path = entry.path();
173        if path.is_file() {
174            res.push(TestFile::from_file(&path));
175        }
176    }
177
178    res
179}