Skip to content

Commit 334ae39

Browse files
authored
feat: Implement rest of II:A (#17)
Implements the indentation requirement of II:A
2 parents ee28ce2 + b911fe5 commit 334ae39

File tree

1 file changed

+284
-11
lines changed

1 file changed

+284
-11
lines changed

src/rules/rule02a.rs

Lines changed: 284 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,81 @@
3232
//! Example: fread(&value, sizeof(double),
3333
//! 1, special_fp);
3434
//! ```
35-
//!
36-
//! # Implementation notes
37-
//!
38-
//! Currently, we only implement the 80-column limit and not the rule that wrapped statements must
39-
//! be indented by 2 extra spaces.
4035
4136
use codespan_reporting::diagnostic::{Diagnostic, Label};
42-
use tree_sitter::Tree;
37+
use indoc::indoc;
38+
use tree_sitter::{Range, Tree};
4339
use unicode_width::UnicodeWidthStr;
4440

45-
use crate::{helpers::LinesWithPosition, rules::api::Rule};
41+
use crate::{
42+
helpers::{LinesWithPosition, QueryHelper},
43+
rules::api::Rule,
44+
};
45+
46+
/// Amount that wrapped lines must be indented, in columns.
47+
const WRAPPED_LINE_INDENT_WIDTH: usize = 2;
4648

4749
/// # Rule II:A.
4850
///
4951
/// See module-level documentation for details.
5052
pub struct Rule02a {}
5153

54+
/// Tree-sitter query for Rule II:A.
55+
const QUERY_STR: &str = indoc! { /* query */ r##"
56+
; If statement condition
57+
(if_statement
58+
condition: (_) @splittable)
59+
60+
; Switch statement condition
61+
(switch_statement
62+
condition: (_) @splittable)
63+
64+
; Case expression
65+
(case_statement
66+
value: (_) @splittable)
67+
68+
; While loop condition
69+
(while_statement
70+
condition: (_) @splittable)
71+
72+
; Do-while loop condition
73+
(do_statement
74+
condition: (_) @splittable)
75+
76+
; For loop parentheses. Here we need separate start and end
77+
; captures since a capture can only capture one node.
78+
(for_statement
79+
"(" @splittable.begin
80+
_
81+
")" @splittable.end)
82+
83+
; Expression statement
84+
(expression_statement) @splittable
85+
86+
; Return statement
87+
(return_statement) @splittable
88+
89+
; Break statement
90+
(break_statement) @splittable
91+
92+
; Continue statement
93+
(continue_statement) @splittable
94+
95+
; Goto statemnt
96+
(goto_statement) @splittable
97+
98+
; Macro definitions
99+
(preproc_function_def
100+
"#define" @splittable.begin
101+
value: (_) @splittable.end)
102+
103+
; Variable initialization
104+
(declaration
105+
declarator: (init_declarator)) @splittable
106+
"## };
107+
52108
impl Rule for Rule02a {
53-
fn check(&self, _tree: &Tree, code_bytes: &[u8]) -> Vec<Diagnostic<()>> {
109+
fn check(&self, tree: &Tree, code_bytes: &[u8]) -> Vec<Diagnostic<()>> {
54110
let mut diagnostics = Vec::new();
55111

56112
// Check for lines >80 columns long
@@ -66,7 +122,88 @@ impl Rule for Rule02a {
66122
}
67123
}
68124

69-
// TODO: Check indentation of wrapped lines
125+
let helper = QueryHelper::new(QUERY_STR, tree, code_bytes);
126+
let splittable_capture_i = helper.expect_index_for_capture("splittable");
127+
let splittable_begin_capture_i = helper.expect_index_for_capture("splittable.begin");
128+
let splittable_end_capture_i = helper.expect_index_for_capture("splittable.end");
129+
helper.for_each_match(|qmatch| {
130+
// Expect either @splittable or a pair of @splittable.begin and @splittable.end
131+
assert!(qmatch.captures.len() == 1 || qmatch.captures.len() == 2);
132+
133+
// Get range from capture
134+
let range = match qmatch.captures.len() {
135+
1 => {
136+
let node = helper.expect_node_for_capture_index(qmatch, splittable_capture_i);
137+
node.range()
138+
}
139+
2 => {
140+
let start_node =
141+
helper.expect_node_for_capture_index(qmatch, splittable_begin_capture_i);
142+
let end_node =
143+
helper.expect_node_for_capture_index(qmatch, splittable_end_capture_i);
144+
Range {
145+
start_byte: start_node.start_byte(),
146+
end_byte: end_node.end_byte(),
147+
start_point: start_node.start_position(),
148+
end_point: end_node.end_position(),
149+
}
150+
}
151+
n => panic!("Expected 1 or 2 captures, got {}", n),
152+
};
153+
154+
// If not split across two lines, skip this match
155+
if range.start_point.row == range.end_point.row {
156+
return;
157+
}
158+
159+
// Check indentation of wrapped lines and construct list of labels
160+
let mut code_lines = LinesWithPosition::from(
161+
std::str::from_utf8(code_bytes).expect("Code is not valid UTF-8"),
162+
)
163+
.skip(range.start_point.row)
164+
.take(range.end_point.row + 1 - range.start_point.row);
165+
let (first_line, first_line_byte_pos) = code_lines.next().unwrap();
166+
let first_line_indent = get_indentation(first_line);
167+
let first_line_indent_width = line_width(first_line_indent);
168+
let expected_indent_width = first_line_indent_width + WRAPPED_LINE_INDENT_WIDTH;
169+
let mut labels = Vec::new();
170+
for (this_line, this_line_pos) in &mut code_lines {
171+
let this_line_indent = get_indentation(this_line);
172+
let this_line_indent_width = line_width(this_line_indent);
173+
if this_line_indent_width < expected_indent_width {
174+
labels.push(
175+
Label::primary((), this_line_pos..(this_line_pos + this_line_indent.len()))
176+
.with_message(format!(
177+
"Expected >={expected_indent_width} columns of indentation on continuing line"
178+
)),
179+
);
180+
}
181+
}
182+
183+
// If no labels, these lines pass the test
184+
if labels.is_empty() {
185+
return;
186+
}
187+
188+
diagnostics.push(
189+
Diagnostic::warning()
190+
.with_code("II:A")
191+
.with_message(format!(
192+
"Wrapped expressions/statements must be indented by at least {} spaces",
193+
WRAPPED_LINE_INDENT_WIDTH
194+
))
195+
.with_labels(labels)
196+
.with_label(
197+
Label::secondary(
198+
(),
199+
first_line_byte_pos..(first_line_byte_pos + first_line_indent.len()),
200+
)
201+
.with_message(format!(
202+
"Found indentation of {first_line_indent_width} columns on initial line",
203+
)),
204+
),
205+
);
206+
});
70207

71208
diagnostics
72209
}
@@ -80,12 +217,22 @@ fn line_width(line: &str) -> usize {
80217
line.width() + line.chars().filter(|c| *c == '\t').count() * 7
81218
}
82219

220+
/// Returns the leading whitespace part of the line
221+
fn get_indentation(line: &str) -> &str {
222+
&line[0..(line.len() - line.trim_start().len())]
223+
}
224+
83225
#[cfg(test)]
84226
mod tests {
85-
// TODO: Test the actual lints produced, because not all of the logic for this rule is
86-
// encapsulated in the query.
227+
use std::process::ExitCode;
87228

229+
use indoc::indoc;
88230
use pretty_assertions::assert_eq;
231+
use tree_sitter::Parser;
232+
233+
use crate::{helpers::testing::test_captures, rules::api::Rule};
234+
235+
use super::{Rule02a, QUERY_STR};
89236

90237
#[test]
91238
fn line_width() {
@@ -108,4 +255,130 @@ mod tests {
108255
assert_eq!(expected, super::line_width(line));
109256
}
110257
}
258+
259+
#[test]
260+
fn test_rule02a_captures() -> ExitCode {
261+
let code = indoc! { /* c */ r#"
262+
int global_var = 10;
263+
//!? splittable
264+
int global_var
265+
//!? splittable
266+
= 10;
267+
268+
#define MAX(a, b) (a < b ? b : a)
269+
//!? splittable.begin
270+
//!? splittable.end
271+
#define MAX(a, b) \
272+
//!? splittable.begin
273+
(a < b ? b : a)
274+
//!? splittable.end
275+
#define MAX(a, b) \
276+
//!? splittable.begin
277+
(a < b ? b : a)
278+
//!? splittable.end
279+
280+
int main() {
281+
int global_var = 10;
282+
//!? splittable
283+
int global_var
284+
//!? splittable
285+
= 10;
286+
287+
x + 2;
288+
//!? splittable
289+
x
290+
//!? splittable
291+
+ 2;
292+
293+
if (something) abort();
294+
//!? splittable
295+
//!? splittable
296+
if (some ||
297+
//!? splittable
298+
thing) abort();
299+
//!? splittable
300+
301+
while (something) abort();
302+
//!? splittable
303+
//!? splittable
304+
while (some ||
305+
//!? splittable
306+
thing) abort();
307+
//!? splittable
308+
309+
for (;;) {}
310+
//!? splittable.begin
311+
//!? splittable.end
312+
313+
printf("This is %s with %s",
314+
//!? splittable
315+
"a format string",
316+
"many lines of arguments");
317+
}
318+
"# };
319+
test_captures(QUERY_STR, code)
320+
}
321+
322+
#[test]
323+
fn test_rule02a_diagnostics() {
324+
let rule = Rule02a {};
325+
let mut parser = Parser::new();
326+
parser.set_language(&tree_sitter_c::LANGUAGE.into()).unwrap();
327+
328+
macro_rules! test {
329+
($code:literal, $ndiag:expr, $nlabels_list:expr) => {
330+
let inner_code = ::indoc::indoc! { $code };
331+
let mut code = String::new();
332+
code.push_str("int main() {\n");
333+
for line in inner_code.lines() {
334+
code.push_str(" ");
335+
code.push_str(line);
336+
code.push('\n');
337+
}
338+
code.push_str("}\n");
339+
dbg!(&code);
340+
let tree = parser.parse(code.as_bytes(), None).unwrap();
341+
let diagnostics = rule.check(&tree, code.as_bytes());
342+
assert_eq!($ndiag, diagnostics.len());
343+
let nlabels_list: &[usize] = &$nlabels_list;
344+
assert_eq!(
345+
nlabels_list,
346+
&diagnostics.iter().map(|diag| diag.labels.len()).collect::<Vec<usize>>()
347+
);
348+
};
349+
}
350+
351+
// Each test takes the code, number of expected diagnostics, and total number of expected
352+
// labels
353+
354+
test!("int x = 0;", 0, []);
355+
test!("int x =\n 0;", 0, []);
356+
test!("int x =\n0;", 1, [2]);
357+
test!("for (int i = 0; i < n; i++) {}", 0, []);
358+
test!("for (int i = 0;\ni < n;\ni++) {}", 1, [3]);
359+
test!("for (int i = 0;\n i < n;\n i++) {}", 0, []);
360+
test!(
361+
"
362+
if (my_condition() == true) {
363+
data->
364+
el->other = false;
365+
}
366+
",
367+
0,
368+
[]
369+
);
370+
test!(
371+
"
372+
if (my_condition()
373+
== true) {
374+
data->
375+
el->other = false;
376+
}
377+
",
378+
0,
379+
[]
380+
);
381+
test!("#define MAX(a, b) \\\n((a) < (b) ? (a) : (b))", 1, [2]);
382+
test!("#define MAX(a, b) \\\n ((a) < (b) ? (a) : (b))", 0, []);
383+
}
111384
}

0 commit comments

Comments
 (0)