@@ -7,7 +7,7 @@ use tower_lsp_server::lsp_types::{
77
88use oxc_data_structures:: rope:: { Rope , get_line_column} ;
99use oxc_diagnostics:: { OxcCode , Severity } ;
10- use oxc_linter:: { Fix , Message , PossibleFixes } ;
10+ use oxc_linter:: { DisableDirectives , Fix , Message , PossibleFixes , RuleCommentType } ;
1111
1212#[ derive( Debug , Clone , Default ) ]
1313pub struct DiagnosticReport {
@@ -38,6 +38,7 @@ pub fn message_to_lsp_diagnostic(
3838 uri : & Uri ,
3939 source_text : & str ,
4040 rope : & Rope ,
41+ directives : Option < & DisableDirectives > ,
4142) -> DiagnosticReport {
4243 let severity = match message. error . severity {
4344 Severity :: Error => Some ( lsp_types:: DiagnosticSeverity :: ERROR ) ,
@@ -130,6 +131,7 @@ pub fn message_to_lsp_diagnostic(
130131 section_offset,
131132 rope,
132133 source_text,
134+ directives,
133135 ) ;
134136
135137 DiagnosticReport { diagnostic, fixed_content }
@@ -203,6 +205,7 @@ fn add_ignore_fixes(
203205 section_offset : u32 ,
204206 rope : & Rope ,
205207 source_text : & str ,
208+ directives : Option < & DisableDirectives > ,
206209) -> PossibleFixContent {
207210 // do not append ignore code actions when the error is the ignore action
208211 if matches ! ( fixes, PossibleFixContent :: Single ( ref fix) if fix. message. as_ref( ) . is_some_and( |message| message. starts_with( "remove unused disable directive" ) ) )
@@ -218,13 +221,13 @@ fn add_ignore_fixes(
218221 }
219222
220223 if let Some ( rule_name) = code. number . as_ref ( ) {
221- // TODO: doesn't support disabling multiple rules by name for a given line.
222224 new_fixes. push ( disable_for_this_line (
223225 rule_name,
224226 error_offset,
225227 section_offset,
226228 rope,
227229 source_text,
230+ directives,
228231 ) ) ;
229232 new_fixes. push ( disable_for_this_section ( rule_name, section_offset, rope, source_text) ) ;
230233 }
@@ -244,6 +247,76 @@ fn disable_for_this_line(
244247 section_offset : u32 ,
245248 rope : & Rope ,
246249 source_text : & str ,
250+ directives : Option < & DisableDirectives > ,
251+ ) -> FixedContent {
252+ if let Some ( directives) = directives
253+ && let Some ( existing_comment) =
254+ directives. find_disable_next_line_comment_for_position ( error_offset)
255+ {
256+ return append_rule_to_existing_comment ( rule_name, existing_comment, rope, source_text) ;
257+ }
258+
259+ create_new_disable_comment ( rule_name, error_offset, section_offset, rope, source_text)
260+ }
261+
262+ /// Append a rule to an existing disable-next-line comment, or return a no-op if the rule already exists.
263+ fn append_rule_to_existing_comment (
264+ rule_name : & str ,
265+ existing_comment : & oxc_linter:: DisableRuleComment ,
266+ rope : & Rope ,
267+ source_text : & str ,
268+ ) -> FixedContent {
269+ let comment_span = existing_comment. span ;
270+ let comment_text = & source_text[ comment_span. start as usize ..comment_span. end as usize ] ;
271+
272+ // Get the existing rules from the comment
273+ let existing_rules: Vec < & str > = match & existing_comment. r#type {
274+ RuleCommentType :: All => {
275+ // If it's an "all" directive, just return a no-op (can't add more rules)
276+ let start_position = offset_to_position ( rope, comment_span. start , source_text) ;
277+ let end_position = offset_to_position ( rope, comment_span. end , source_text) ;
278+ return FixedContent {
279+ message : Some ( format ! ( "Disable {rule_name} for this line" ) ) ,
280+ code : comment_text. to_string ( ) ,
281+ range : Range :: new ( start_position, end_position) ,
282+ } ;
283+ }
284+ RuleCommentType :: Single ( rules) => rules. iter ( ) . map ( |r| r. rule_name . as_str ( ) ) . collect ( ) ,
285+ } ;
286+
287+ // Check if the rule is already in the comment
288+ if existing_rules. contains ( & rule_name) {
289+ // Rule already exists, return a no-op fix (same content)
290+ let start_position = offset_to_position ( rope, comment_span. start , source_text) ;
291+ let end_position = offset_to_position ( rope, comment_span. end , source_text) ;
292+ return FixedContent {
293+ message : Some ( format ! ( "Disable {rule_name} for this line" ) ) ,
294+ code : comment_text. to_string ( ) ,
295+ range : Range :: new ( start_position, end_position) ,
296+ } ;
297+ }
298+
299+ // Append the new rule to the comment using comma separation (ESLint standard format)
300+ // The comment_text is just the content inside the comment (without // prefix for line comments)
301+ let new_comment = format ! ( "{comment_text}, {rule_name}" ) ;
302+
303+ let start_position = offset_to_position ( rope, comment_span. start , source_text) ;
304+ let end_position = offset_to_position ( rope, comment_span. end , source_text) ;
305+
306+ FixedContent {
307+ message : Some ( format ! ( "Disable {rule_name} for this line" ) ) ,
308+ code : new_comment,
309+ range : Range :: new ( start_position, end_position) ,
310+ }
311+ }
312+
313+ /// Create a new disable-next-line comment when no existing comment is found.
314+ fn create_new_disable_comment (
315+ rule_name : & str ,
316+ error_offset : u32 ,
317+ section_offset : u32 ,
318+ rope : & Rope ,
319+ source_text : & str ,
247320) -> FixedContent {
248321 let bytes = source_text. as_bytes ( ) ;
249322 // Find the line break before the error
@@ -436,7 +509,7 @@ mod test {
436509 fn disable_for_this_line_single_line ( ) {
437510 let source = "console.log('hello');" ;
438511 let rope = Rope :: from_str ( source) ;
439- let fix = super :: disable_for_this_line ( "no-console" , 0 , 0 , & rope, source) ;
512+ let fix = super :: disable_for_this_line ( "no-console" , 0 , 0 , & rope, source, None ) ;
440513
441514 assert_eq ! ( fix. code, "// oxlint-disable-next-line no-console\n " ) ;
442515 assert_eq ! ( fix. range. start. line, 0 ) ;
@@ -447,7 +520,7 @@ mod test {
447520 fn disable_for_this_line_with_spaces ( ) {
448521 let source = " console.log('hello');" ;
449522 let rope = Rope :: from_str ( source) ;
450- let fix = super :: disable_for_this_line ( "no-console" , 10 , 0 , & rope, source) ;
523+ let fix = super :: disable_for_this_line ( "no-console" , 10 , 0 , & rope, source, None ) ;
451524
452525 assert_eq ! ( fix. code, " // oxlint-disable-next-line no-console\n " ) ;
453526 assert_eq ! ( fix. range. start. line, 0 ) ;
@@ -458,7 +531,7 @@ mod test {
458531 fn disable_for_this_line_with_tabs ( ) {
459532 let source = "\t \t console.log('hello');" ;
460533 let rope = Rope :: from_str ( source) ;
461- let fix = super :: disable_for_this_line ( "no-console" , 10 , 0 , & rope, source) ;
534+ let fix = super :: disable_for_this_line ( "no-console" , 10 , 0 , & rope, source, None ) ;
462535
463536 assert_eq ! ( fix. code, "\t \t // oxlint-disable-next-line no-console\n " ) ;
464537 assert_eq ! ( fix. range. start. line, 0 ) ;
@@ -469,7 +542,7 @@ mod test {
469542 fn disable_for_this_line_mixed_tabs_spaces ( ) {
470543 let source = "\t \t console.log('hello');" ;
471544 let rope = Rope :: from_str ( source) ;
472- let fix = super :: disable_for_this_line ( "no-console" , 12 , 0 , & rope, source) ;
545+ let fix = super :: disable_for_this_line ( "no-console" , 12 , 0 , & rope, source, None ) ;
473546
474547 assert_eq ! ( fix. code, "\t \t // oxlint-disable-next-line no-console\n " ) ;
475548 assert_eq ! ( fix. range. start. line, 0 ) ;
@@ -480,7 +553,7 @@ mod test {
480553 fn disable_for_this_line_multiline_with_tabs ( ) {
481554 let source = "function test() {\n \t console.log('hello');\n }" ;
482555 let rope = Rope :: from_str ( source) ;
483- let fix = super :: disable_for_this_line ( "no-console" , 27 , 0 , & rope, source) ;
556+ let fix = super :: disable_for_this_line ( "no-console" , 27 , 0 , & rope, source, None ) ;
484557
485558 assert_eq ! ( fix. code, "\t // oxlint-disable-next-line no-console\n " ) ;
486559 assert_eq ! ( fix. range. start. line, 1 ) ;
@@ -491,7 +564,7 @@ mod test {
491564 fn disable_for_this_line_multiline_with_spaces ( ) {
492565 let source = "function test() {\n console.log('hello');\n }" ;
493566 let rope = Rope :: from_str ( source) ;
494- let fix = super :: disable_for_this_line ( "no-console" , 30 , 0 , & rope, source) ;
567+ let fix = super :: disable_for_this_line ( "no-console" , 30 , 0 , & rope, source, None ) ;
495568
496569 assert_eq ! ( fix. code, " // oxlint-disable-next-line no-console\n " ) ;
497570 assert_eq ! ( fix. range. start. line, 1 ) ;
@@ -502,7 +575,7 @@ mod test {
502575 fn disable_for_this_line_complex_indentation ( ) {
503576 let source = "function test() {\n \t \t console.log('hello');\n }" ;
504577 let rope = Rope :: from_str ( source) ;
505- let fix = super :: disable_for_this_line ( "no-console" , 33 , 0 , & rope, source) ;
578+ let fix = super :: disable_for_this_line ( "no-console" , 33 , 0 , & rope, source, None ) ;
506579
507580 assert_eq ! ( fix. code, "\t \t // oxlint-disable-next-line no-console\n " ) ;
508581 assert_eq ! ( fix. range. start. line, 1 ) ;
@@ -513,7 +586,7 @@ mod test {
513586 fn disable_for_this_line_no_indentation ( ) {
514587 let source = "function test() {\n console.log('hello');\n }" ;
515588 let rope = Rope :: from_str ( source) ;
516- let fix = super :: disable_for_this_line ( "no-console" , 26 , 0 , & rope, source) ;
589+ let fix = super :: disable_for_this_line ( "no-console" , 26 , 0 , & rope, source, None ) ;
517590
518591 assert_eq ! ( fix. code, "// oxlint-disable-next-line no-console\n " ) ;
519592 assert_eq ! ( fix. range. start. line, 1 ) ;
@@ -524,7 +597,7 @@ mod test {
524597 fn disable_for_this_line_crlf_with_tabs ( ) {
525598 let source = "function test() {\r \n \t console.log('hello');\r \n }" ;
526599 let rope = Rope :: from_str ( source) ;
527- let fix = super :: disable_for_this_line ( "no-console" , 28 , 0 , & rope, source) ;
600+ let fix = super :: disable_for_this_line ( "no-console" , 28 , 0 , & rope, source, None ) ;
528601
529602 assert_eq ! ( fix. code, "\t // oxlint-disable-next-line no-console\n " ) ;
530603 assert_eq ! ( fix. range. start. line, 1 ) ;
@@ -535,7 +608,7 @@ mod test {
535608 fn disable_for_this_line_deeply_nested ( ) {
536609 let source = "if (true) {\n \t \t if (nested) {\n \t \t \t console.log('deep');\n \t \t }\n }" ;
537610 let rope = Rope :: from_str ( source) ;
538- let fix = super :: disable_for_this_line ( "no-console" , 40 , 0 , & rope, source) ;
611+ let fix = super :: disable_for_this_line ( "no-console" , 40 , 0 , & rope, source, None ) ;
539612
540613 assert_eq ! ( fix. code, "\t \t \t // oxlint-disable-next-line no-console\n " ) ;
541614 assert_eq ! ( fix. range. start. line, 2 ) ;
@@ -546,7 +619,7 @@ mod test {
546619 fn disable_for_this_line_at_start_of_file ( ) {
547620 let source = "console.log('hello');" ;
548621 let rope = Rope :: from_str ( source) ;
549- let fix = super :: disable_for_this_line ( "no-console" , 0 , 0 , & rope, source) ;
622+ let fix = super :: disable_for_this_line ( "no-console" , 0 , 0 , & rope, source, None ) ;
550623
551624 assert_eq ! ( fix. code, "// oxlint-disable-next-line no-console\n " ) ;
552625 assert_eq ! ( fix. range. start. line, 0 ) ;
@@ -559,7 +632,7 @@ mod test {
559632 let source = "function test() {\n \t code \t here\n }" ;
560633 let rope = Rope :: from_str ( source) ;
561634 // Error at position of 'code' (after " \t")
562- let fix = super :: disable_for_this_line ( "no-console" , 21 , 0 , & rope, source) ;
635+ let fix = super :: disable_for_this_line ( "no-console" , 21 , 0 , & rope, source, None ) ;
563636
564637 // Should only capture " \t" at the beginning, not the spaces around "here"
565638 assert_eq ! ( fix. code, " \t // oxlint-disable-next-line no-console\n " ) ;
@@ -574,8 +647,14 @@ mod test {
574647 let rope = Rope :: from_str ( source) ;
575648 let section_offset = 8 ; // At the \n after "<script>"
576649 let error_offset = 17 ; // At 'console'
577- let fix =
578- super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
650+ let fix = super :: disable_for_this_line (
651+ "no-console" ,
652+ error_offset,
653+ section_offset,
654+ & rope,
655+ source,
656+ None ,
657+ ) ;
579658
580659 assert_eq ! ( fix. code, "// oxlint-disable-next-line no-console\n " ) ;
581660 assert_eq ! ( fix. range. start. line, 1 ) ;
@@ -589,8 +668,14 @@ mod test {
589668 let rope = Rope :: from_str ( source) ;
590669 let section_offset = 8 ; // After "<script>"
591670 let error_offset = 16 ; // At 'console'
592- let fix =
593- super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
671+ let fix = super :: disable_for_this_line (
672+ "no-console" ,
673+ error_offset,
674+ section_offset,
675+ & rope,
676+ source,
677+ None ,
678+ ) ;
594679
595680 assert_eq ! ( fix. code, "\n // oxlint-disable-next-line no-console\n " ) ;
596681 assert_eq ! ( fix. range. start. line, 0 ) ;
@@ -604,8 +689,14 @@ mod test {
604689 let rope = Rope :: from_str ( source) ;
605690 let section_offset = 31 ; // At \n after "<script>"
606691 let error_offset = 36 ; // At 'console' (after " ")
607- let fix =
608- super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
692+ let fix = super :: disable_for_this_line (
693+ "no-console" ,
694+ error_offset,
695+ section_offset,
696+ & rope,
697+ source,
698+ None ,
699+ ) ;
609700
610701 assert_eq ! ( fix. code, " // oxlint-disable-next-line no-console\n " ) ;
611702 assert_eq ! ( fix. range. start. line, 3 ) ;
@@ -619,8 +710,14 @@ mod test {
619710 let rope = Rope :: from_str ( source) ;
620711 let section_offset = 8 ; // At the \n after "<script>"
621712 let error_offset = 8 ; // Error exactly at section offset
622- let fix =
623- super :: disable_for_this_line ( "no-console" , error_offset, section_offset, & rope, source) ;
713+ let fix = super :: disable_for_this_line (
714+ "no-console" ,
715+ error_offset,
716+ section_offset,
717+ & rope,
718+ source,
719+ None ,
720+ ) ;
624721
625722 assert_eq ! ( fix. code, "// oxlint-disable-next-line no-console\n " ) ;
626723 assert_eq ! ( fix. range. start. line, 1 ) ;
0 commit comments