@@ -250,15 +250,18 @@ pub struct Suppression {
250250 /// This may differ from the line the suppression applies to
251251 /// (e.g., when the comment is on the line above).
252252 comment_line : LineNumber ,
253+ /// Byte offset within `comment_line` of the `#` that starts the comment.
254+ comment_offset : usize ,
253255}
254256
255257impl Suppression {
256258 /// A blanket suppression for `tool` that matches every error code.
257- fn blanket ( tool : Tool , comment_line : LineNumber ) -> Self {
259+ fn blanket ( tool : Tool , comment_line : LineNumber , comment_offset : usize ) -> Self {
258260 Self {
259261 tool,
260262 kind : Vec :: new ( ) ,
261263 comment_line,
264+ comment_offset,
262265 }
263266 }
264267
@@ -267,6 +270,11 @@ impl Suppression {
267270 self . comment_line
268271 }
269272
273+ /// Returns the byte offset of the comment's `#` within `comment_line`.
274+ pub fn comment_offset ( & self ) -> usize {
275+ self . comment_offset
276+ }
277+
270278 /// Returns the error codes that this suppression applies to.
271279 /// An empty slice means the suppression applies to all error codes.
272280 pub fn error_codes ( & self ) -> & [ String ] {
@@ -304,20 +312,18 @@ impl Ignore {
304312 for ( idx, line_str) in code. lines ( ) . enumerate ( ) {
305313 let ( comment_start, new_state) = find_comment_start ( line_str, in_triple_quote) ;
306314 in_triple_quote = new_state;
307- let comments = if let Some ( comment_start) = comment_start {
308- & line_str[ comment_start..]
309- } else {
310- ""
311- } ;
312315 let is_comment_only_line = comment_start
313316 . is_some_and ( |comment_start| line_str[ ..comment_start] . trim_start ( ) . is_empty ( ) ) ;
314317 line = LineNumber :: from_zero_indexed ( idx as u32 ) ;
315318 if !pending. is_empty ( ) && ( line_str. is_empty ( ) || !is_comment_only_line) {
316319 ignores. entry ( line) . or_default ( ) . append ( & mut pending) ;
317320 }
318- // We know `#` is at the beginning, so the first split is an empty string
319- for x in comments. split ( '#' ) . skip ( 1 ) {
320- if let Some ( supp) = Self :: parse_ignore_comment ( x, line) {
321+ let Some ( comment_start) = comment_start else {
322+ continue ;
323+ } ;
324+ // We know `#` is at `comment_start`, so the first split is an empty string
325+ for x in line_str[ comment_start..] . split ( '#' ) . skip ( 1 ) {
326+ if let Some ( supp) = Self :: parse_ignore_comment ( x, line, comment_start) {
321327 if is_comment_only_line {
322328 pending. push ( supp) ;
323329 } else {
@@ -336,8 +342,12 @@ impl Ignore {
336342 }
337343
338344 /// Given the content of a comment, parse it as a suppression.
339- /// The comment_line parameter indicates which line the comment is on.
340- fn parse_ignore_comment ( l : & str , comment_line : LineNumber ) -> Option < Suppression > {
345+ /// `comment_line` and `comment_offset` locate the `#` starting the comment.
346+ fn parse_ignore_comment (
347+ l : & str ,
348+ comment_line : LineNumber ,
349+ comment_offset : usize ,
350+ ) -> Option < Suppression > {
341351 let mut lex = Lexer ( l) ;
342352 lex. trim_start ( ) ;
343353
@@ -361,9 +371,10 @@ impl Ignore {
361371 tool,
362372 kind : parse_error_codes ( inside) ,
363373 comment_line,
374+ comment_offset,
364375 } ) ;
365376 } else if gap || lex. word_boundary ( ) {
366- return Some ( Suppression :: blanket ( tool, comment_line) ) ;
377+ return Some ( Suppression :: blanket ( tool, comment_line, comment_offset ) ) ;
367378 }
368379 None
369380 }
@@ -509,20 +520,22 @@ pub fn parse_ignore_all(
509520 break ;
510521 }
511522
512- if let Some ( ( tool, prev_line) ) = prev_ignore {
523+ if let Some ( ( tool, prev_line, prev_offset ) ) = prev_ignore {
513524 // The previous `# type: ignore` was followed by another comment or
514525 // blank line, so it is a whole-file suppression.
515- res. push ( Suppression :: blanket ( tool, prev_line) ) ;
526+ res. push ( Suppression :: blanket ( tool, prev_line, prev_offset ) ) ;
516527 prev_ignore = None ;
517528 }
518529
519530 let mut lex = Lexer ( trimmed) ;
520531 if !lex. starts_with ( "#" ) {
521532 continue ;
522533 }
534+ // `trimmed` starts with `#`, so its offset is the line's leading whitespace.
535+ let comment_offset = raw_line. len ( ) - raw_line. trim_start ( ) . len ( ) ;
523536 lex. trim_start ( ) ;
524537 if lex. starts_with ( "pyre-ignore-all-errors" ) {
525- res. push ( Suppression :: blanket ( Tool :: Pyre , line) ) ;
538+ res. push ( Suppression :: blanket ( Tool :: Pyre , line, comment_offset ) ) ;
526539 } else if let Some ( tool) = lex. starts_with_tool ( ) {
527540 lex. trim_start ( ) ;
528541 if lex. starts_with ( "ignore-errors" ) {
@@ -554,12 +567,13 @@ pub fn parse_ignore_all(
554567 tool,
555568 kind,
556569 comment_line : line,
570+ comment_offset,
557571 } ) ;
558572 }
559573 } else if !seen_docstring && lex. starts_with ( "ignore" ) && lex. blank ( ) {
560574 // After a docstring, bare `# type: ignore` is not recognized
561575 // as an ignore-all directive.
562- prev_ignore = Some ( ( tool, line) ) ;
576+ prev_ignore = Some ( ( tool, line, comment_offset ) ) ;
563577 }
564578 }
565579 }
@@ -648,16 +662,39 @@ x = """
648662 f ( "x = '''''' # pyrefly: ignore" , & [ ( Tool :: Pyrefly , 1 ) ] ) ;
649663 }
650664
665+ #[ test]
666+ fn test_suppression_comment_offset ( ) {
667+ fn f ( x : & str , expect : & [ ( u32 , usize ) ] ) {
668+ assert_eq ! (
669+ & Ignore :: parse_ignores( x)
670+ . into_iter( )
671+ . flat_map( |( _, xs) | xs. map( |x| ( x. comment_line. get( ) , x. comment_offset) ) )
672+ . collect:: <Vec <_>>( ) ,
673+ expect,
674+ "{x:?}"
675+ ) ;
676+ }
677+
678+ f ( "x = 1 # type: ignore" , & [ ( 1 , 7 ) ] ) ;
679+ // Not the `#` inside the string literal
680+ f ( r##"x: str = "#hash" # type: ignore"## , & [ ( 1 , 18 ) ] ) ;
681+ // Line starts inside a triple-quoted string that closes mid-line
682+ f ( "x = \" \" \" \n #fake\" \" \" # type: ignore" , & [ ( 2 , 9 ) ] ) ;
683+ // A comment above code keeps its own line and offset
684+ f ( " # type: ignore\n x = 1" , & [ ( 1 , 2 ) ] ) ;
685+ }
686+
651687 #[ test]
652688 fn test_parse_ignore_comment ( ) {
653689 fn f ( x : & str , tool : Option < Tool > , kind : & [ & str ] ) {
654690 let dummy_line = LineNumber :: default ( ) ;
655691 assert_eq ! (
656- Ignore :: parse_ignore_comment( x, dummy_line) ,
692+ Ignore :: parse_ignore_comment( x, dummy_line, 0 ) ,
657693 tool. map( |tool| Suppression {
658694 tool,
659695 kind: kind. map( |x| ( * x) . to_owned( ) ) ,
660696 comment_line: dummy_line,
697+ comment_offset: 0 ,
661698 } ) ,
662699 "{x:?}"
663700 ) ;
@@ -750,6 +787,7 @@ x = """
750787 tool: x. 0 ,
751788 kind: x. 2 . iter( ) . map( |x| ( * x) . to_owned( ) ) . collect( ) ,
752789 comment_line: LineNumber :: new( x. 1 ) . unwrap( ) ,
790+ comment_offset: 0 ,
753791 } )
754792 . collect:: <Vec <_>>( ) ,
755793 "{x:?}"
@@ -837,6 +875,7 @@ x = """
837875 tool: x. 0 ,
838876 kind: x. 2 . iter( ) . map( |x| ( * x) . to_owned( ) ) . collect( ) ,
839877 comment_line: LineNumber :: new( x. 1 ) . unwrap( ) ,
878+ comment_offset: 0 ,
840879 } )
841880 . collect:: <Vec <_>>( ) ,
842881 "{x:?}"
0 commit comments