libpfu_style/
spacing.rs

1//! Spaces and newline checks.
2
3use anyhow::Result;
4use async_trait::async_trait;
5use itertools::Itertools;
6use libabbs::apml::lst;
7use libpfu::{
8	Linter, Session, declare_lint, declare_linter,
9	message::{LintMessage, Snippet},
10	walk_apml,
11};
12use log::debug;
13
14declare_linter! {
15	pub EXTRA_SPACES_LINTER,
16	ExtraSpacesLinter,
17	["extra-spaces"]
18}
19
20declare_lint! {
21	pub EXTRA_SPACES_LINT,
22	"extra-spaces",
23	Warning,
24	"extra spaces should be removed"
25}
26
27#[async_trait]
28impl Linter for ExtraSpacesLinter {
29	async fn apply(&self, sess: &Session) -> Result<()> {
30		for mut apml in walk_apml(sess) {
31			debug!("Looking for extra spaces in {apml:?}");
32			let mut ranges = apml
33				.lst()
34				.0
35				.iter()
36				.enumerate()
37				.batching(|iter| {
38					let mut ret = Some(
39						iter.take_while(|(_, token)| {
40							!matches!(token, lst::Token::Newline)
41						})
42						.collect_vec(),
43					);
44					// discard newline
45					_ = iter.next();
46					ret.take_if(|tokens| !tokens.is_empty())
47				})
48				.filter(|line| {
49					matches!(line.first(), Some((_, lst::Token::Spacy(_))))
50						|| matches!(
51							line.last(),
52							Some((_, lst::Token::Spacy(_)))
53						)
54				})
55				.inspect(|line| {
56					let index = line[0].0;
57					LintMessage::new(EXTRA_SPACES_LINT)
58						.snippet(Snippet::new_index(sess, &apml, index))
59						.emit(sess);
60				})
61				.map(|line| {
62					let mut before = 0;
63					while let Some((_, lst::Token::Spacy(_))) = line.get(before)
64					{
65						before += 1;
66					}
67					let mut after = 0;
68					while let Some((_, lst::Token::Spacy(_))) =
69						line.get(line.len() - after)
70					{
71						after += 1;
72					}
73					let first_idx = line.first().unwrap().0;
74					let last_idx = line.last().unwrap().0;
75					(first_idx..first_idx + before, last_idx - after..last_idx)
76				})
77				.collect_vec();
78			debug!(
79				"Found {} lines with extra spaces in {:?}",
80				ranges.len(),
81				apml
82			);
83			if !sess.dry && !ranges.is_empty() {
84				// ranges must be reversed to avoid removing earlier ranges
85				// from invalidating later ranger
86				ranges.reverse();
87				apml.with_upgraded(|apml| {
88					apml.with_lst(|lst| {
89						for (range1, range2) in ranges {
90							lst.0.drain(range2);
91							lst.0.drain(range1);
92						}
93					});
94				});
95			}
96		}
97		Ok(())
98	}
99}