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
4136use codespan_reporting:: diagnostic:: { Diagnostic , Label } ;
42- use tree_sitter:: Tree ;
37+ use indoc:: indoc;
38+ use tree_sitter:: { Range , Tree } ;
4339use 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.
5052pub 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+
52108impl 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) ]
84226mod 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 =\n 0;" , 1 , [ 2 ] ) ;
357+ test ! ( "for (int i = 0; i < n; i++) {}" , 0 , [ ] ) ;
358+ test ! ( "for (int i = 0;\n i < n;\n i++) {}" , 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