14
14
use itertools:: Itertools ;
15
15
use lazy_static:: lazy_static;
16
16
use regex:: Regex ;
17
+ use walkdir:: WalkDir ;
17
18
use std:: collections:: HashMap ;
18
19
use std:: ffi:: OsStr ;
19
20
use std:: fs;
@@ -35,6 +36,7 @@ lazy_static! {
35
36
pub static ref DOCS_LINK : String = "https://rust-lang-nursery.github.io/rust-clippy/master/index.html" . to_string( ) ;
36
37
}
37
38
39
+ /// Lint data parsed from the Clippy source code.
38
40
#[ derive( Clone , PartialEq , Debug ) ]
39
41
pub struct Lint {
40
42
pub name : String ,
@@ -55,22 +57,39 @@ impl Lint {
55
57
}
56
58
}
57
59
58
- /// Returns all non-deprecated lints
59
- pub fn active_lints ( lints : impl Iterator < Item =Self > ) -> impl Iterator < Item =Self > {
60
- lints. filter ( |l| l. deprecation . is_none ( ) )
60
+ /// Returns all non-deprecated lints and non-internal lints
61
+ pub fn usable_lints ( lints : impl Iterator < Item =Self > ) -> impl Iterator < Item =Self > {
62
+ lints. filter ( |l| l. deprecation . is_none ( ) && !l . is_internal ( ) )
61
63
}
62
64
63
65
/// Returns the lints in a HashMap, grouped by the different lint groups
64
66
pub fn by_lint_group ( lints : & [ Self ] ) -> HashMap < String , Vec < Self > > {
65
67
lints. iter ( ) . map ( |lint| ( lint. group . to_string ( ) , lint. clone ( ) ) ) . into_group_map ( )
66
68
}
69
+
70
+ pub fn is_internal ( & self ) -> bool {
71
+ self . group . starts_with ( "internal" )
72
+ }
73
+ }
74
+
75
+ pub fn gen_changelog_lint_list ( lints : Vec < Lint > ) -> Vec < String > {
76
+ let mut lint_list_sorted: Vec < Lint > = lints;
77
+ lint_list_sorted. sort_by_key ( |l| l. name . clone ( ) ) ;
78
+ lint_list_sorted
79
+ . iter ( )
80
+ . filter ( |l| !l. is_internal ( ) )
81
+ . map ( |l| {
82
+ format ! ( "[`{}`]: {}#{}" , l. name, DOCS_LINK . clone( ) , l. name)
83
+ } )
84
+ . collect ( )
67
85
}
68
86
87
+ /// Gathers all files in `src/clippy_lints` and gathers all lints inside
69
88
pub fn gather_all ( ) -> impl Iterator < Item =Lint > {
70
89
lint_files ( ) . flat_map ( |f| gather_from_file ( & f) )
71
90
}
72
91
73
- fn gather_from_file ( dir_entry : & fs :: DirEntry ) -> impl Iterator < Item =Lint > {
92
+ fn gather_from_file ( dir_entry : & walkdir :: DirEntry ) -> impl Iterator < Item =Lint > {
74
93
let mut file = fs:: File :: open ( dir_entry. path ( ) ) . unwrap ( ) ;
75
94
let mut content = String :: new ( ) ;
76
95
file. read_to_string ( & mut content) . unwrap ( ) ;
@@ -89,13 +108,98 @@ fn parse_contents(content: &str, filename: &str) -> impl Iterator<Item=Lint> {
89
108
}
90
109
91
110
/// Collects all .rs files in the `clippy_lints/src` directory
92
- fn lint_files ( ) -> impl Iterator < Item =fs:: DirEntry > {
93
- fs:: read_dir ( "../clippy_lints/src" )
94
- . unwrap ( )
111
+ fn lint_files ( ) -> impl Iterator < Item =walkdir:: DirEntry > {
112
+ // We use `WalkDir` instead of `fs::read_dir` here in order to recurse into subdirectories.
113
+ // Otherwise we would not collect all the lints, for example in `clippy_lints/src/methods/`.
114
+ WalkDir :: new ( "../clippy_lints/src" )
115
+ . into_iter ( )
95
116
. filter_map ( |f| f. ok ( ) )
96
117
. filter ( |f| f. path ( ) . extension ( ) == Some ( OsStr :: new ( "rs" ) ) )
97
118
}
98
119
120
+ /// Replace a region in a file delimited by two lines matching regexes.
121
+ ///
122
+ /// `path` is the relative path to the file on which you want to perform the replacement.
123
+ ///
124
+ /// See `replace_region_in_text` for documentation of the other options.
125
+ #[ allow( clippy:: expect_fun_call) ]
126
+ pub fn replace_region_in_file < F > ( path : & str , start : & str , end : & str , replace_start : bool , replacements : F ) where F : Fn ( ) -> Vec < String > {
127
+ let mut f = fs:: File :: open ( path) . expect ( & format ! ( "File not found: {}" , path) ) ;
128
+ let mut contents = String :: new ( ) ;
129
+ f. read_to_string ( & mut contents) . expect ( "Something went wrong reading the file" ) ;
130
+ let replaced = replace_region_in_text ( & contents, start, end, replace_start, replacements) ;
131
+
132
+ let mut f = fs:: File :: create ( path) . expect ( & format ! ( "File not found: {}" , path) ) ;
133
+ f. write_all ( replaced. as_bytes ( ) ) . expect ( "Unable to write file" ) ;
134
+ // Ensure we write the changes with a trailing newline so that
135
+ // the file has the proper line endings.
136
+ f. write_all ( b"\n " ) . expect ( "Unable to write file" ) ;
137
+ }
138
+
139
+ /// Replace a region in a text delimited by two lines matching regexes.
140
+ ///
141
+ /// * `text` is the input text on which you want to perform the replacement
142
+ /// * `start` is a `&str` that describes the delimiter line before the region you want to replace.
143
+ /// As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
144
+ /// * `end` is a `&str` that describes the delimiter line until where the replacement should
145
+ /// happen. As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
146
+ /// * If `replace_start` is true, the `start` delimiter line is replaced as well.
147
+ /// The `end` delimiter line is never replaced.
148
+ /// * `replacements` is a closure that has to return a `Vec<String>` which contains the new text.
149
+ ///
150
+ /// If you want to perform the replacement on files instead of already parsed text,
151
+ /// use `replace_region_in_file`.
152
+ ///
153
+ /// # Example
154
+ ///
155
+ /// ```
156
+ /// let the_text = "replace_start\nsome text\nthat will be replaced\nreplace_end";
157
+ /// let result = clippy_dev::replace_region_in_text(
158
+ /// the_text,
159
+ /// r#"replace_start"#,
160
+ /// r#"replace_end"#,
161
+ /// false,
162
+ /// || {
163
+ /// vec!["a different".to_string(), "text".to_string()]
164
+ /// }
165
+ /// );
166
+ /// assert_eq!("replace_start\na different\ntext\nreplace_end", result);
167
+ /// ```
168
+ pub fn replace_region_in_text < F > ( text : & str , start : & str , end : & str , replace_start : bool , replacements : F ) -> String where F : Fn ( ) -> Vec < String > {
169
+ let lines = text. lines ( ) ;
170
+ let mut in_old_region = false ;
171
+ let mut found = false ;
172
+ let mut new_lines = vec ! [ ] ;
173
+ let start = Regex :: new ( start) . unwrap ( ) ;
174
+ let end = Regex :: new ( end) . unwrap ( ) ;
175
+
176
+ for line in lines {
177
+ if in_old_region {
178
+ if end. is_match ( & line) {
179
+ in_old_region = false ;
180
+ new_lines. extend ( replacements ( ) ) ;
181
+ new_lines. push ( line. to_string ( ) ) ;
182
+ }
183
+ } else if start. is_match ( & line) {
184
+ if !replace_start {
185
+ new_lines. push ( line. to_string ( ) ) ;
186
+ }
187
+ in_old_region = true ;
188
+ found = true ;
189
+ } else {
190
+ new_lines. push ( line. to_string ( ) ) ;
191
+ }
192
+ }
193
+
194
+ if !found {
195
+ // This happens if the provided regex in `clippy_dev/src/main.rs` is not found in the
196
+ // given text or file. Most likely this is an error on the programmer's side and the Regex
197
+ // is incorrect.
198
+ println ! ( "regex {:?} not found. You may have to update it." , start) ;
199
+ }
200
+ new_lines. join ( "\n " )
201
+ }
202
+
99
203
#[ test]
100
204
fn test_parse_contents ( ) {
101
205
let result: Vec < Lint > = parse_contents (
@@ -136,15 +240,54 @@ declare_deprecated_lint! {
136
240
}
137
241
138
242
#[ test]
139
- fn test_active_lints ( ) {
243
+ fn test_replace_region ( ) {
244
+ let text = r#"
245
+ abc
246
+ 123
247
+ 789
248
+ def
249
+ ghi"# ;
250
+ let expected = r#"
251
+ abc
252
+ hello world
253
+ def
254
+ ghi"# ;
255
+ let result = replace_region_in_text ( text, r#"^\s*abc$"# , r#"^\s*def"# , false , || {
256
+ vec ! [ "hello world" . to_string( ) ]
257
+ } ) ;
258
+ assert_eq ! ( expected, result) ;
259
+ }
260
+
261
+ #[ test]
262
+ fn test_replace_region_with_start ( ) {
263
+ let text = r#"
264
+ abc
265
+ 123
266
+ 789
267
+ def
268
+ ghi"# ;
269
+ let expected = r#"
270
+ hello world
271
+ def
272
+ ghi"# ;
273
+ let result = replace_region_in_text ( text, r#"^\s*abc$"# , r#"^\s*def"# , true , || {
274
+ vec ! [ "hello world" . to_string( ) ]
275
+ } ) ;
276
+ assert_eq ! ( expected, result) ;
277
+ }
278
+
279
+ #[ test]
280
+ fn test_usable_lints ( ) {
140
281
let lints = vec ! [
141
282
Lint :: new( "should_assert_eq" , "Deprecated" , "abc" , Some ( "Reason" ) , "module_name" ) ,
142
- Lint :: new( "should_assert_eq2" , "Not Deprecated" , "abc" , None , "module_name" )
283
+ Lint :: new( "should_assert_eq2" , "Not Deprecated" , "abc" , None , "module_name" ) ,
284
+ Lint :: new( "should_assert_eq2" , "internal" , "abc" , None , "module_name" ) ,
285
+ Lint :: new( "should_assert_eq2" , "internal_style" , "abc" , None , "module_name" )
143
286
] ;
144
287
let expected = vec ! [
145
288
Lint :: new( "should_assert_eq2" , "Not Deprecated" , "abc" , None , "module_name" )
146
289
] ;
147
- assert_eq ! ( expected, Lint :: active_lints ( lints. into_iter( ) ) . collect:: <Vec <Lint >>( ) ) ;
290
+ assert_eq ! ( expected, Lint :: usable_lints ( lints. into_iter( ) ) . collect:: <Vec <Lint >>( ) ) ;
148
291
}
149
292
150
293
#[ test]
@@ -164,3 +307,17 @@ fn test_by_lint_group() {
164
307
] ) ;
165
308
assert_eq ! ( expected, Lint :: by_lint_group( & lints) ) ;
166
309
}
310
+
311
+ #[ test]
312
+ fn test_gen_changelog_lint_list ( ) {
313
+ let lints = vec ! [
314
+ Lint :: new( "should_assert_eq" , "group1" , "abc" , None , "module_name" ) ,
315
+ Lint :: new( "should_assert_eq2" , "group2" , "abc" , None , "module_name" ) ,
316
+ Lint :: new( "incorrect_internal" , "internal_style" , "abc" , None , "module_name" ) ,
317
+ ] ;
318
+ let expected = vec ! [
319
+ format!( "[`should_assert_eq`]: {}#should_assert_eq" , DOCS_LINK . to_string( ) ) ,
320
+ format!( "[`should_assert_eq2`]: {}#should_assert_eq2" , DOCS_LINK . to_string( ) )
321
+ ] ;
322
+ assert_eq ! ( expected, gen_changelog_lint_list( lints) ) ;
323
+ }
0 commit comments