1
- use ruff_diagnostics:: { Diagnostic , Violation } ;
1
+ use ruff_diagnostics:: { Diagnostic , Edit , Fix , FixAvailability , Violation } ;
2
2
use ruff_macros:: { derive_message_formats, violation} ;
3
3
use ruff_python_index:: Indexer ;
4
+ use ruff_python_trivia:: Cursor ;
4
5
use ruff_source_file:: Locator ;
5
- use ruff_text_size:: Ranged ;
6
+ use ruff_text_size:: { Ranged , TextRange , TextSize } ;
6
7
7
8
use crate :: noqa:: Directive ;
8
9
@@ -27,15 +28,56 @@ use crate::noqa::Directive;
27
28
/// from .base import * # noqa: F403
28
29
/// ```
29
30
///
31
+ /// ## Fix safety
32
+ /// This rule will attempt to fix blanket `noqa` annotations that appear to
33
+ /// be unintentional. For example, given `# noqa F401`, the rule will suggest
34
+ /// inserting a colon, as in `# noqa: F401`.
35
+ ///
36
+ /// While modifying `noqa` comments is generally safe, doing so may introduce
37
+ /// additional diagnostics.
38
+ ///
30
39
/// ## References
31
40
/// - [Ruff documentation](https://docs.astral.sh/ruff/configuration/#error-suppression)
32
41
#[ violation]
33
- pub struct BlanketNOQA ;
42
+ pub struct BlanketNOQA {
43
+ missing_colon : bool ,
44
+ space_before_colon : bool ,
45
+ }
34
46
35
47
impl Violation for BlanketNOQA {
48
+ const FIX_AVAILABILITY : FixAvailability = FixAvailability :: Sometimes ;
49
+
36
50
#[ derive_message_formats]
37
51
fn message ( & self ) -> String {
38
- format ! ( "Use specific rule codes when using `noqa`" )
52
+ let BlanketNOQA {
53
+ missing_colon,
54
+ space_before_colon,
55
+ } = self ;
56
+
57
+ // This awkward branching is necessary to ensure that the generic message is picked up by
58
+ // `derive_message_formats`.
59
+ if !missing_colon && !space_before_colon {
60
+ format ! ( "Use specific rule codes when using `noqa`" )
61
+ } else if * missing_colon {
62
+ format ! ( "Use a colon when specifying `noqa` rule codes" )
63
+ } else {
64
+ format ! ( "Do not add spaces between `noqa` and its colon" )
65
+ }
66
+ }
67
+
68
+ fn fix_title ( & self ) -> Option < String > {
69
+ let BlanketNOQA {
70
+ missing_colon,
71
+ space_before_colon,
72
+ } = self ;
73
+
74
+ if * missing_colon {
75
+ Some ( "Add missing colon" . to_string ( ) )
76
+ } else if * space_before_colon {
77
+ Some ( "Remove space(s) before colon" . to_string ( ) )
78
+ } else {
79
+ None
80
+ }
39
81
}
40
82
}
41
83
@@ -47,8 +89,54 @@ pub(crate) fn blanket_noqa(
47
89
) {
48
90
for range in indexer. comment_ranges ( ) {
49
91
let line = locator. slice ( * range) ;
50
- if let Ok ( Some ( Directive :: All ( all) ) ) = Directive :: try_extract ( line, range. start ( ) ) {
51
- diagnostics. push ( Diagnostic :: new ( BlanketNOQA , all. range ( ) ) ) ;
92
+ let offset = range. start ( ) ;
93
+ if let Ok ( Some ( Directive :: All ( all) ) ) = Directive :: try_extract ( line, TextSize :: new ( 0 ) ) {
94
+ // The `all` range is that of the `noqa` directive in, e.g., `# noqa` or `# noqa F401`.
95
+ let noqa_start = offset + all. start ( ) ;
96
+ let noqa_end = offset + all. end ( ) ;
97
+
98
+ // Skip the `# noqa`, plus any trailing whitespace.
99
+ let mut cursor = Cursor :: new ( & line[ all. end ( ) . to_usize ( ) ..] ) ;
100
+ cursor. eat_while ( char:: is_whitespace) ;
101
+
102
+ // Check for extraneous spaces before the colon.
103
+ // Ex) `# noqa : F401`
104
+ if cursor. first ( ) == ':' {
105
+ let start = offset + all. end ( ) ;
106
+ let end = start + cursor. token_len ( ) ;
107
+ let mut diagnostic = Diagnostic :: new (
108
+ BlanketNOQA {
109
+ missing_colon : false ,
110
+ space_before_colon : true ,
111
+ } ,
112
+ TextRange :: new ( noqa_start, end) ,
113
+ ) ;
114
+ diagnostic. set_fix ( Fix :: unsafe_edit ( Edit :: deletion ( start, end) ) ) ;
115
+ diagnostics. push ( diagnostic) ;
116
+ } else if Directive :: lex_code ( cursor. chars ( ) . as_str ( ) ) . is_some ( ) {
117
+ // Check for a missing colon.
118
+ // Ex) `# noqa F401`
119
+ let start = offset + all. end ( ) ;
120
+ let end = start + TextSize :: new ( 1 ) ;
121
+ let mut diagnostic = Diagnostic :: new (
122
+ BlanketNOQA {
123
+ missing_colon : true ,
124
+ space_before_colon : false ,
125
+ } ,
126
+ TextRange :: new ( noqa_start, end) ,
127
+ ) ;
128
+ diagnostic. set_fix ( Fix :: unsafe_edit ( Edit :: insertion ( ':' . to_string ( ) , start) ) ) ;
129
+ diagnostics. push ( diagnostic) ;
130
+ } else {
131
+ // Otherwise, it looks like an intentional blanket `noqa` annotation.
132
+ diagnostics. push ( Diagnostic :: new (
133
+ BlanketNOQA {
134
+ missing_colon : false ,
135
+ space_before_colon : false ,
136
+ } ,
137
+ TextRange :: new ( noqa_start, noqa_end) ,
138
+ ) ) ;
139
+ }
52
140
}
53
141
}
54
142
}
0 commit comments