libpfu_style/
empty_line.rs

1//! Empty-line 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 EMPTY_LINE_LINTER,
16	EmptyLineLinter,
17	[
18		"missing-trailing-line",
19		"too-many-trailing-empty-lines",
20		"too-many-empty-lines",
21	]
22}
23
24declare_lint! {
25	pub MISSING_TRAILING_LINE_LINT,
26	"missing-trailing-line",
27	Warning,
28	"missing empty line at the end"
29}
30
31declare_lint! {
32	pub TOO_MANY_TRAILING_EMPTY_LINES,
33	"too-many-trailing-empty-lines",
34	Warning,
35	"too many trailing empty lines"
36}
37
38declare_lint! {
39	pub TOO_MANY_EMPTY_LINES,
40	"too-many-empty-lines",
41	Warning,
42	"more than two empty lines"
43}
44
45#[async_trait]
46impl Linter for EmptyLineLinter {
47	async fn apply(&self, sess: &Session) -> Result<()> {
48		for mut apml in walk_apml(sess) {
49			{
50				debug!("Looking for missing trailing new lines in {apml:?}");
51				let missing_new_line = apml
52					.lst()
53					.0
54					.iter()
55					.rev()
56					.take_while(|token| !matches!(token, lst::Token::Newline))
57					.any(|token| token.is_empty());
58				if missing_new_line {
59					LintMessage::new(MISSING_TRAILING_LINE_LINT)
60						.snippet(Snippet::new_index(
61							sess,
62							&apml,
63							apml.lst().0.len() - 1,
64						))
65						.emit(sess);
66					if !sess.dry {
67						apml.with_upgraded(|apml| {
68							apml.with_lst(|lst| lst.0.push(lst::Token::Newline))
69						});
70					}
71				}
72			}
73			{
74				debug!("Counting trailing empty lines in {apml:?}");
75				let trailing_newlines = apml
76					.lst()
77					.0
78					.iter()
79					.enumerate()
80					.rev()
81					.take_while(|(_, token)| token.is_empty())
82					.filter(|(_, token)| matches!(token, lst::Token::Newline))
83					.collect_vec();
84				if trailing_newlines.len() > 1 {
85					LintMessage::new(TOO_MANY_TRAILING_EMPTY_LINES)
86						.snippet(Snippet::new_index(
87							sess,
88							&apml,
89							apml.lst().0.len() - 1,
90						))
91						.emit(sess);
92					if !sess.dry {
93						let start = trailing_newlines.first().unwrap().0 + 1;
94						apml.with_upgraded(|apml| {
95							apml.with_lst(|lst| lst.0.truncate(start - 1))
96						});
97					}
98				}
99			}
100			{
101				debug!("Counting continuous empty lines in {apml:?}");
102				enum State {
103					NotEmpty,
104					Empty { from: usize, lines: usize },
105				}
106				let mut state = State::NotEmpty;
107				let mut ranges = Vec::new();
108				for (idx, token) in apml.lst().0.iter().enumerate() {
109					if token.is_empty() {
110						match state {
111							State::NotEmpty => {
112								state = State::Empty {
113									from: idx,
114									lines: 0,
115								}
116							}
117							State::Empty { from: _, lines: _ } => {}
118						}
119						if matches!(token, lst::Token::Newline) {
120							if let State::Empty { from: _, lines } = &mut state
121							{
122								*lines += 1;
123							} else {
124								unreachable!()
125							}
126						}
127					} else {
128						match state {
129							State::NotEmpty => {}
130							State::Empty { from, lines } => {
131								state = State::NotEmpty;
132								if lines > 2 {
133									LintMessage::new(TOO_MANY_EMPTY_LINES)
134										.snippet(Snippet::new_index(
135											sess, &apml, from,
136										))
137										.emit(sess);
138									ranges.push(from..idx);
139								}
140							}
141						}
142					}
143				}
144				// newlines at the end of file is handled in previous check
145				// so skipping them here
146				if !sess.dry {
147					ranges.reverse();
148					if !ranges.is_empty() {
149						apml.with_upgraded(|apml| {
150							apml.with_lst(|lst| {
151								for range in ranges {
152									lst.0.drain(range.start..range.end);
153									lst.0.insert(
154										range.start,
155										lst::Token::Newline,
156									);
157									lst.0.insert(
158										range.start,
159										lst::Token::Newline,
160									);
161								}
162							})
163						});
164					}
165				}
166			}
167		}
168		Ok(())
169	}
170}