@@ -187,7 +187,6 @@ impl<'a> SpanPositionMessage<'a> {
187187 Self { start, end, message : None }
188188 }
189189
190- #[ must_use]
191190 pub fn with_message ( mut self , message : Option < Cow < ' a , str > > ) -> Self {
192191 self . message = message;
193192 self
@@ -362,7 +361,13 @@ fn add_ignore_fixes<'a>(
362361
363362 if let Some ( rule_name) = code. number . as_ref ( ) {
364363 // TODO: doesn't support disabling multiple rules by name for a given line.
365- new_fixes. push ( disable_for_this_line ( rule_name, error_offset, rope, source_text) ) ;
364+ new_fixes. push ( disable_for_this_line (
365+ rule_name,
366+ error_offset,
367+ section_offset,
368+ rope,
369+ source_text,
370+ ) ) ;
366371 new_fixes. push ( disable_for_this_section ( rule_name, section_offset, rope, source_text) ) ;
367372 }
368373
@@ -378,15 +383,43 @@ fn add_ignore_fixes<'a>(
378383fn disable_for_this_line < ' a > (
379384 rule_name : & str ,
380385 error_offset : u32 ,
386+ section_offset : u32 ,
381387 rope : & Rope ,
382388 source_text : & str ,
383389) -> FixWithPosition < ' a > {
384- let mut start_position = offset_to_position ( rope, error_offset, source_text) ;
385- start_position. character = 0 ; // TODO: character should be set to match the first non-whitespace character in the source text to match the existing indentation.
386- let end_position = start_position. clone ( ) ;
390+ let bytes = source_text. as_bytes ( ) ;
391+ // Find the line break before the error
392+ let mut line_break_offset = error_offset;
393+ for byte in bytes[ section_offset as usize ..error_offset as usize ] . iter ( ) . rev ( ) {
394+ if * byte == b'\n' || * byte == b'\r' {
395+ break ;
396+ }
397+ line_break_offset -= 1 ;
398+ }
399+
400+ // For framework files, ensure we don't go before the section start
401+ if section_offset > 0 && line_break_offset < section_offset {
402+ line_break_offset = section_offset;
403+ }
404+
405+ let ( content_prefix, insert_offset) =
406+ get_section_insert_position ( section_offset, line_break_offset, bytes) ;
407+
408+ let whitespace_range = {
409+ let start = insert_offset as usize ;
410+ let end = error_offset as usize ;
411+ let slice = & bytes[ start..end] ;
412+ let whitespace_len = slice. iter ( ) . take_while ( |c| matches ! ( c, b' ' | b'\t' ) ) . count ( ) ;
413+ & slice[ ..whitespace_len]
414+ } ;
415+ let whitespace_string = String :: from_utf8_lossy ( whitespace_range) ;
416+
417+ let position = offset_to_position ( rope, insert_offset, source_text) ;
387418 FixWithPosition {
388- content : Cow :: Owned ( format ! ( "// oxlint-disable-next-line {rule_name}\n " ) ) ,
389- span : SpanPositionMessage :: new ( start_position, end_position)
419+ content : Cow :: Owned ( format ! (
420+ "{content_prefix}{whitespace_string}// oxlint-disable-next-line {rule_name}\n "
421+ ) ) ,
422+ span : SpanPositionMessage :: new ( position. clone ( ) , position)
390423 . with_message ( Some ( Cow :: Owned ( format ! ( "Disable {rule_name} for this line" ) ) ) ) ,
391424 }
392425}
@@ -399,37 +432,57 @@ fn disable_for_this_section<'a>(
399432) -> FixWithPosition < ' a > {
400433 let comment = format ! ( "// oxlint-disable {rule_name}\n " ) ;
401434
402- let ( content, offset) = if section_offset == 0 {
403- // JS files - insert at the beginning
404- ( Cow :: Owned ( comment) , section_offset)
405- } else {
435+ let ( content_prefix, insert_offset) =
436+ get_section_insert_position ( section_offset, section_offset, source_text. as_bytes ( ) ) ;
437+
438+ let content = Cow :: Owned ( format ! ( "{content_prefix}{comment}" ) ) ;
439+ let position = offset_to_position ( rope, insert_offset, source_text) ;
440+
441+ FixWithPosition {
442+ content,
443+ span : SpanPositionMessage :: new ( position. clone ( ) , position)
444+ . with_message ( Some ( Cow :: Owned ( format ! ( "Disable {rule_name} for this file" ) ) ) ) ,
445+ }
446+ }
447+
448+ /// Get the insert position and content prefix for section-based insertions.
449+ ///
450+ /// For framework files (section_offset > 0), this handles proper line break detection.
451+ /// For regular JS files (section_offset == 0), it returns the offset as-is.
452+ ///
453+ /// Returns (content_prefix, insert_offset) where:
454+ /// - content_prefix: "\n" if we need to add a line break, "" otherwise
455+ /// - insert_offset: the byte offset where the content should be inserted
456+ fn get_section_insert_position (
457+ section_offset : u32 ,
458+ target_offset : u32 ,
459+ bytes : & [ u8 ] ,
460+ ) -> ( & ' static str , u32 ) {
461+ if section_offset == 0 {
462+ // Regular JS files - insert at target offset
463+ ( "" , target_offset)
464+ } else if target_offset == section_offset {
406465 // Framework files - check for line breaks at section_offset
407- let bytes = source_text. as_bytes ( ) ;
408466 let current = bytes. get ( section_offset as usize ) ;
409467 let next = bytes. get ( ( section_offset + 1 ) as usize ) ;
410468
411469 match ( current, next) {
412470 ( Some ( b'\n' ) , _) => {
413471 // LF at offset, insert after it
414- ( Cow :: Owned ( comment ) , section_offset + 1 )
472+ ( "" , section_offset + 1 )
415473 }
416474 ( Some ( b'\r' ) , Some ( b'\n' ) ) => {
417475 // CRLF at offset, insert after both
418- ( Cow :: Owned ( comment ) , section_offset + 2 )
476+ ( "" , section_offset + 2 )
419477 }
420478 _ => {
421479 // Not at line start, prepend newline
422- ( Cow :: Owned ( "\n " . to_owned ( ) + & comment ) , section_offset)
480+ ( "\n " , section_offset)
423481 }
424482 }
425- } ;
426-
427- let position = offset_to_position ( rope, offset, source_text) ;
428-
429- FixWithPosition {
430- content,
431- span : SpanPositionMessage :: new ( position. clone ( ) , position)
432- . with_message ( Some ( Cow :: Owned ( format ! ( "Disable {rule_name} for this file" ) ) ) ) ,
483+ } else {
484+ // Framework files where target_offset != section_offset (line was found)
485+ ( "" , target_offset)
433486 }
434487}
435488
@@ -518,6 +571,186 @@ mod test {
518571 assert_eq ! ( fix. span. start. character, 6 ) ;
519572 }
520573
574+ #[ test]
575+ fn disable_for_this_line_single_line ( ) {
576+ let source = "console.log('hello');" ;
577+ let rope = Rope :: from_str ( source) ;
578+ let fix = super :: disable_for_this_line ( "no-console" , 0 , 0 , & rope, source) ;
579+
580+ assert_eq ! ( fix. content, "// oxlint-disable-next-line no-console\n " ) ;
581+ assert_eq ! ( fix. span. start. line, 0 ) ;
582+ assert_eq ! ( fix. span. start. character, 0 ) ;
583+ }
584+
585+ #[ test]
586+ fn disable_for_this_line_with_spaces ( ) {
587+ let source = " console.log('hello');" ;
588+ let rope = Rope :: from_str ( source) ;
589+ let fix = super :: disable_for_this_line ( "no-console" , 10 , 0 , & rope, source) ;
590+
591+ assert_eq ! ( fix. content, " // oxlint-disable-next-line no-console\n " ) ;
592+ assert_eq ! ( fix. span. start. line, 0 ) ;
593+ assert_eq ! ( fix. span. start. character, 0 ) ;
594+ }
595+
596+ #[ test]
597+ fn disable_for_this_line_with_tabs ( ) {
598+ let source = "\t \t console.log('hello');" ;
599+ let rope = Rope :: from_str ( source) ;
600+ let fix = super :: disable_for_this_line ( "no-console" , 10 , 0 , & rope, source) ;
601+
602+ assert_eq ! ( fix. content, "\t \t // oxlint-disable-next-line no-console\n " ) ;
603+ assert_eq ! ( fix. span. start. line, 0 ) ;
604+ assert_eq ! ( fix. span. start. character, 0 ) ;
605+ }
606+
607+ #[ test]
608+ fn disable_for_this_line_mixed_tabs_spaces ( ) {
609+ let source = "\t \t console.log('hello');" ;
610+ let rope = Rope :: from_str ( source) ;
611+ let fix = super :: disable_for_this_line ( "no-console" , 12 , 0 , & rope, source) ;
612+
613+ assert_eq ! ( fix. content, "\t \t // oxlint-disable-next-line no-console\n " ) ;
614+ assert_eq ! ( fix. span. start. line, 0 ) ;
615+ assert_eq ! ( fix. span. start. character, 0 ) ;
616+ }
617+
618+ #[ test]
619+ fn disable_for_this_line_multiline_with_tabs ( ) {
620+ let source = "function test() {\n \t console.log('hello');\n }" ;
621+ let rope = Rope :: from_str ( source) ;
622+ let fix = super :: disable_for_this_line ( "no-console" , 27 , 0 , & rope, source) ;
623+
624+ assert_eq ! ( fix. content, "\t // oxlint-disable-next-line no-console\n " ) ;
625+ assert_eq ! ( fix. span. start. line, 1 ) ;
626+ assert_eq ! ( fix. span. start. character, 0 ) ;
627+ }
628+
629+ #[ test]
630+ fn disable_for_this_line_multiline_with_spaces ( ) {
631+ let source = "function test() {\n console.log('hello');\n }" ;
632+ let rope = Rope :: from_str ( source) ;
633+ let fix = super :: disable_for_this_line ( "no-console" , 30 , 0 , & rope, source) ;
634+
635+ assert_eq ! ( fix. content, " // oxlint-disable-next-line no-console\n " ) ;
636+ assert_eq ! ( fix. span. start. line, 1 ) ;
637+ assert_eq ! ( fix. span. start. character, 0 ) ;
638+ }
639+
640+ #[ test]
641+ fn disable_for_this_line_complex_indentation ( ) {
642+ let source = "function test() {\n \t \t console.log('hello');\n }" ;
643+ let rope = Rope :: from_str ( source) ;
644+ let fix = super :: disable_for_this_line ( "no-console" , 33 , 0 , & rope, source) ;
645+
646+ assert_eq ! ( fix. content, "\t \t // oxlint-disable-next-line no-console\n " ) ;
647+ assert_eq ! ( fix. span. start. line, 1 ) ;
648+ assert_eq ! ( fix. span. start. character, 0 ) ;
649+ }
650+
651+ #[ test]
652+ fn disable_for_this_line_no_indentation ( ) {
653+ let source = "function test() {\n console.log('hello');\n }" ;
654+ let rope = Rope :: from_str ( source) ;
655+ let fix = super :: disable_for_this_line ( "no-console" , 26 , 0 , & rope, source) ;
656+
657+ assert_eq ! ( fix. content, "// oxlint-disable-next-line no-console\n " ) ;
658+ assert_eq ! ( fix. span. start. line, 1 ) ;
659+ assert_eq ! ( fix. span. start. character, 0 ) ;
660+ }
661+
662+ #[ test]
663+ fn disable_for_this_line_crlf_with_tabs ( ) {
664+ let source = "function test() {\r \n \t console.log('hello');\r \n }" ;
665+ let rope = Rope :: from_str ( source) ;
666+ let fix = super :: disable_for_this_line ( "no-console" , 28 , 0 , & rope, source) ;
667+
668+ assert_eq ! ( fix. content, "\t // oxlint-disable-next-line no-console\n " ) ;
669+ assert_eq ! ( fix. span. start. line, 1 ) ;
670+ assert_eq ! ( fix. span. start. character, 0 ) ;
671+ }
672+
673+ #[ test]
674+ fn disable_for_this_line_deeply_nested ( ) {
675+ let source = "if (true) {\n \t \t if (nested) {\n \t \t \t console.log('deep');\n \t \t }\n }" ;
676+ let rope = Rope :: from_str ( source) ;
677+ let fix = super :: disable_for_this_line ( "no-console" , 40 , 0 , & rope, source) ;
678+
679+ assert_eq ! ( fix. content, "\t \t \t // oxlint-disable-next-line no-console\n " ) ;
680+ assert_eq ! ( fix. span. start. line, 2 ) ;
681+ assert_eq ! ( fix. span. start. character, 0 ) ;
682+ }
683+
684+ #[ test]
685+ fn disable_for_this_line_at_start_of_file ( ) {
686+ let source = "console.log('hello');" ;
687+ let rope = Rope :: from_str ( source) ;
688+ let fix = super :: disable_for_this_line ( "no-console" , 0 , 0 , & rope, source) ;
689+
690+ assert_eq ! ( fix. content, "// oxlint-disable-next-line no-console\n " ) ;
691+ assert_eq ! ( fix. span. start. line, 0 ) ;
692+ assert_eq ! ( fix. span. start. character, 0 ) ;
693+ }
694+
695+ #[ test]
696+ fn disable_for_this_line_whitespace_only_continuous ( ) {
697+ // Test that only continuous whitespace from line start is captured
698+ let source = "function test() {\n \t code \t here\n }" ;
699+ let rope = Rope :: from_str ( source) ;
700+ // Error at position of 'code' (after " \t")
701+ let fix = super :: disable_for_this_line ( "no-console" , 21 , 0 , & rope, source) ;
702+
703+ // Should only capture " \t" at the beginning, not the spaces around "here"
704+ assert_eq ! ( fix. content, " \t // oxlint-disable-next-line no-console\n " ) ;
705+ assert_eq ! ( fix. span. start. line, 1 ) ;
706+ assert_eq ! ( fix. span. start. character, 0 ) ;
707+ }
708+
709+ #[ test]
710+ fn disable_for_this_line_with_section_offset ( ) {
711+ // Test framework file with section offset (like Vue/Svelte)
712+ let source = "<script>\n console.log('hello');\n </script>" ;
713+ let rope = Rope :: from_str ( source) ;
714+ let section_offset = 8 ; // At the \n after "<script>"
715+ let error_offset = 17 ; // At 'console'
716+ let fix =
717+ super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
718+
719+ assert_eq ! ( fix. content, "// oxlint-disable-next-line no-console\n " ) ;
720+ assert_eq ! ( fix. span. start. line, 1 ) ;
721+ assert_eq ! ( fix. span. start. character, 0 ) ;
722+ }
723+
724+ #[ test]
725+ fn disable_for_this_line_section_offset_mid_line ( ) {
726+ // Test framework file where section starts mid-line
727+ let source = "<script>console.log('hello');\n </script>" ;
728+ let rope = Rope :: from_str ( source) ;
729+ let section_offset = 8 ; // After "<script>"
730+ let error_offset = 16 ; // At 'console'
731+ let fix =
732+ super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
733+
734+ assert_eq ! ( fix. content, "\n // oxlint-disable-next-line no-console\n " ) ;
735+ assert_eq ! ( fix. span. start. line, 0 ) ;
736+ assert_eq ! ( fix. span. start. character, 8 ) ;
737+ }
738+
739+ #[ test]
740+ fn disable_for_this_line_section_offset_with_indentation ( ) {
741+ // Test framework file with indented code
742+ let source = "<template>\n </template>\n <script>\n console.log('hello');\n </script>" ;
743+ let rope = Rope :: from_str ( source) ;
744+ let section_offset = 31 ; // At \n after "<script>"
745+ let error_offset = 36 ; // At 'console' (after " ")
746+ let fix =
747+ super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
748+
749+ assert_eq ! ( fix. content, " // oxlint-disable-next-line no-console\n " ) ;
750+ assert_eq ! ( fix. span. start. line, 3 ) ;
751+ assert_eq ! ( fix. span. start. character, 0 ) ;
752+ }
753+
521754 fn assert_position ( source : & str , offset : u32 , expected : ( u32 , u32 ) ) {
522755 let position = offset_to_position ( & Rope :: from_str ( source) , offset, source) ;
523756 assert_eq ! ( position. line, expected. 0 ) ;
0 commit comments