Skip to content

Commit c111d93

Browse files
committed
fix(language_server): add whitespace for // oxlint-disable-next-line fix
1 parent 6e52bbd commit c111d93

File tree

6 files changed

+168
-13
lines changed

6 files changed

+168
-13
lines changed

crates/oxc_language_server/src/snapshots/[email protected]

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ fixed: Multiple(
101101
message: Some(
102102
"Disable no-debugger for this line",
103103
),
104-
code: "// oxlint-disable-next-line no-debugger\n",
104+
code: " // oxlint-disable-next-line no-debugger\n",
105105
range: Range {
106106
start: Position {
107107
line: 10,
@@ -165,7 +165,7 @@ fixed: Multiple(
165165
message: Some(
166166
"Disable no-debugger for this line",
167167
),
168-
code: "// oxlint-disable-next-line no-debugger\n",
168+
code: " // oxlint-disable-next-line no-debugger\n",
169169
range: Range {
170170
start: Position {
171171
line: 14,
@@ -229,7 +229,7 @@ fixed: Multiple(
229229
message: Some(
230230
"Disable no-debugger for this line",
231231
),
232-
code: "// oxlint-disable-next-line no-debugger\n",
232+
code: " // oxlint-disable-next-line no-debugger\n",
233233
range: Range {
234234
start: Position {
235235
line: 18,

crates/oxc_language_server/src/snapshots/[email protected]

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fixed: Multiple(
2121
message: Some(
2222
"Disable no-extra-boolean-cast for this line",
2323
),
24-
code: "// oxlint-disable-next-line no-extra-boolean-cast\n",
24+
code: " // oxlint-disable-next-line no-extra-boolean-cast\n",
2525
range: Range {
2626
start: Position {
2727
line: 3,
@@ -72,7 +72,7 @@ fixed: Multiple(
7272
message: Some(
7373
"Disable no-non-null-asserted-optional-chain for this line",
7474
),
75-
code: "// oxlint-disable-next-line no-non-null-asserted-optional-chain\n",
75+
code: " // oxlint-disable-next-line no-non-null-asserted-optional-chain\n",
7676
range: Range {
7777
start: Position {
7878
line: 11,

crates/oxc_language_server/src/snapshots/[email protected]

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fixed: Multiple(
2121
message: Some(
2222
"Disable no-unassigned-vars for this line",
2323
),
24-
code: "// oxlint-disable-next-line no-unassigned-vars\n",
24+
code: "\t// oxlint-disable-next-line no-unassigned-vars\n",
2525
range: Range {
2626
start: Position {
2727
line: 3,
@@ -69,7 +69,7 @@ fixed: Multiple(
6969
message: Some(
7070
"Disable no-unassigned-vars for this line",
7171
),
72-
code: "// oxlint-disable-next-line no-unassigned-vars\n",
72+
code: "\t// oxlint-disable-next-line no-unassigned-vars\n",
7373
range: Range {
7474
start: Position {
7575
line: 4,
@@ -133,7 +133,7 @@ fixed: Multiple(
133133
message: Some(
134134
"Disable no-debugger for this line",
135135
),
136-
code: "// oxlint-disable-next-line no-debugger\n",
136+
code: "\t// oxlint-disable-next-line no-debugger\n",
137137
range: Range {
138138
start: Position {
139139
line: 1,

crates/oxc_language_server/src/snapshots/[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ fixed: Multiple(
8585
message: Some(
8686
"Disable no-debugger for this line",
8787
),
88-
code: "// oxlint-disable-next-line no-debugger\n",
88+
code: " // oxlint-disable-next-line no-debugger\n",
8989
range: Range {
9090
start: Position {
9191
line: 2,

crates/oxc_language_server/src/snapshots/[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ fixed: Multiple(
101101
message: Some(
102102
"Disable no-debugger for this line",
103103
),
104-
code: "// oxlint-disable-next-line no-debugger\n",
104+
code: " // oxlint-disable-next-line no-debugger\n",
105105
range: Range {
106106
start: Position {
107107
line: 8,

crates/oxc_linter/src/lsp.rs

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,31 @@ fn disable_for_this_line<'a>(
207207
rope: &Rope,
208208
source_text: &str,
209209
) -> FixWithPosition<'a> {
210-
let mut start_position = offset_to_position(rope, error_offset, source_text);
211-
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.
210+
// 1. search for the first line break character before the error_offset in source_text (if any)
211+
// 2. count all whitespace characters after the line break character
212+
// 3. set start_position.character to the line break character
213+
// 4. add the same whitespace characters to the content of the fix
214+
let bytes = source_text.as_bytes()[..error_offset as usize].iter().rev();
215+
let mut line_break_offset = error_offset;
216+
for byte in bytes {
217+
if *byte == b'\n' || *byte == b'\r' {
218+
break;
219+
}
220+
line_break_offset -= 1;
221+
}
222+
let whitespace_string = source_text
223+
.as_bytes()
224+
.get(line_break_offset as usize..error_offset as usize)
225+
.and_then(|slice| std::str::from_utf8(slice).ok())
226+
.map(|s| s.chars().take_while(|c| c.is_whitespace()).collect::<String>())
227+
.unwrap_or_default();
228+
229+
let start_position = offset_to_position(rope, line_break_offset, source_text);
212230
let end_position = start_position.clone();
213231
FixWithPosition {
214-
content: Cow::Owned(format!("// oxlint-disable-next-line {rule_name}\n")),
232+
content: Cow::Owned(format!(
233+
"{whitespace_string}// oxlint-disable-next-line {rule_name}\n"
234+
)),
215235
span: SpanPositionMessage::new(start_position, end_position)
216236
.with_message(Some(Cow::Owned(format!("Disable {rule_name} for this line")))),
217237
}
@@ -344,6 +364,141 @@ mod test {
344364
assert_eq!(fix.span.start.character, 6);
345365
}
346366

367+
#[test]
368+
fn disable_for_this_line_single_line() {
369+
let source = "console.log('hello');";
370+
let rope = Rope::from_str(source);
371+
let fix = super::disable_for_this_line("no-console", 8, &rope, source);
372+
373+
assert_eq!(fix.content, "// oxlint-disable-next-line no-console\n");
374+
assert_eq!(fix.span.start.line, 0);
375+
assert_eq!(fix.span.start.character, 0);
376+
}
377+
378+
#[test]
379+
fn disable_for_this_line_with_spaces() {
380+
let source = " console.log('hello');";
381+
let rope = Rope::from_str(source);
382+
let fix = super::disable_for_this_line("no-console", 10, &rope, source);
383+
384+
assert_eq!(fix.content, " // oxlint-disable-next-line no-console\n");
385+
assert_eq!(fix.span.start.line, 0);
386+
assert_eq!(fix.span.start.character, 0);
387+
}
388+
389+
#[test]
390+
fn disable_for_this_line_with_tabs() {
391+
let source = "\t\tconsole.log('hello');";
392+
let rope = Rope::from_str(source);
393+
let fix = super::disable_for_this_line("no-console", 10, &rope, source);
394+
395+
assert_eq!(fix.content, "\t\t// oxlint-disable-next-line no-console\n");
396+
assert_eq!(fix.span.start.line, 0);
397+
assert_eq!(fix.span.start.character, 0);
398+
}
399+
400+
#[test]
401+
fn disable_for_this_line_mixed_tabs_spaces() {
402+
let source = "\t \tconsole.log('hello');";
403+
let rope = Rope::from_str(source);
404+
let fix = super::disable_for_this_line("no-console", 12, &rope, source);
405+
406+
assert_eq!(fix.content, "\t \t// oxlint-disable-next-line no-console\n");
407+
assert_eq!(fix.span.start.line, 0);
408+
assert_eq!(fix.span.start.character, 0);
409+
}
410+
411+
#[test]
412+
fn disable_for_this_line_multiline_with_tabs() {
413+
let source = "function test() {\n\tconsole.log('hello');\n}";
414+
let rope = Rope::from_str(source);
415+
let fix = super::disable_for_this_line("no-console", 27, &rope, source);
416+
417+
assert_eq!(fix.content, "\t// oxlint-disable-next-line no-console\n");
418+
assert_eq!(fix.span.start.line, 1);
419+
assert_eq!(fix.span.start.character, 0);
420+
}
421+
422+
#[test]
423+
fn disable_for_this_line_multiline_with_spaces() {
424+
let source = "function test() {\n console.log('hello');\n}";
425+
let rope = Rope::from_str(source);
426+
let fix = super::disable_for_this_line("no-console", 30, &rope, source);
427+
428+
assert_eq!(fix.content, " // oxlint-disable-next-line no-console\n");
429+
assert_eq!(fix.span.start.line, 1);
430+
assert_eq!(fix.span.start.character, 0);
431+
}
432+
433+
#[test]
434+
fn disable_for_this_line_complex_indentation() {
435+
let source = "function test() {\n\t \t console.log('hello');\n}";
436+
let rope = Rope::from_str(source);
437+
let fix = super::disable_for_this_line("no-console", 33, &rope, source);
438+
439+
assert_eq!(fix.content, "\t \t // oxlint-disable-next-line no-console\n");
440+
assert_eq!(fix.span.start.line, 1);
441+
assert_eq!(fix.span.start.character, 0);
442+
}
443+
444+
#[test]
445+
fn disable_for_this_line_no_indentation() {
446+
let source = "function test() {\nconsole.log('hello');\n}";
447+
let rope = Rope::from_str(source);
448+
let fix = super::disable_for_this_line("no-console", 26, &rope, source);
449+
450+
assert_eq!(fix.content, "// oxlint-disable-next-line no-console\n");
451+
assert_eq!(fix.span.start.line, 1);
452+
assert_eq!(fix.span.start.character, 0);
453+
}
454+
455+
#[test]
456+
fn disable_for_this_line_crlf_with_tabs() {
457+
let source = "function test() {\r\n\tconsole.log('hello');\r\n}";
458+
let rope = Rope::from_str(source);
459+
let fix = super::disable_for_this_line("no-console", 28, &rope, source);
460+
461+
assert_eq!(fix.content, "\t// oxlint-disable-next-line no-console\n");
462+
assert_eq!(fix.span.start.line, 1);
463+
assert_eq!(fix.span.start.character, 0);
464+
}
465+
466+
#[test]
467+
fn disable_for_this_line_deeply_nested() {
468+
let source = "if (true) {\n\t\tif (nested) {\n\t\t\tconsole.log('deep');\n\t\t}\n}";
469+
let rope = Rope::from_str(source);
470+
let fix = super::disable_for_this_line("no-console", 40, &rope, source);
471+
472+
assert_eq!(fix.content, "\t\t\t// oxlint-disable-next-line no-console\n");
473+
assert_eq!(fix.span.start.line, 2);
474+
assert_eq!(fix.span.start.character, 0);
475+
}
476+
477+
#[test]
478+
fn disable_for_this_line_at_start_of_file() {
479+
let source = "console.log('hello');";
480+
let rope = Rope::from_str(source);
481+
let fix = super::disable_for_this_line("no-console", 0, &rope, source);
482+
483+
assert_eq!(fix.content, "// oxlint-disable-next-line no-console\n");
484+
assert_eq!(fix.span.start.line, 0);
485+
assert_eq!(fix.span.start.character, 0);
486+
}
487+
488+
#[test]
489+
fn disable_for_this_line_whitespace_only_continuous() {
490+
// Test that only continuous whitespace from line start is captured
491+
let source = "function test() {\n \tcode \there\n}";
492+
let rope = Rope::from_str(source);
493+
// Error at position of 'code' (after " \t")
494+
let fix = super::disable_for_this_line("no-console", 21, &rope, source);
495+
496+
// Should only capture " \t" at the beginning, not the spaces around "here"
497+
assert_eq!(fix.content, " \t// oxlint-disable-next-line no-console\n");
498+
assert_eq!(fix.span.start.line, 1);
499+
assert_eq!(fix.span.start.character, 0);
500+
}
501+
347502
fn assert_position(source: &str, offset: u32, expected: (u32, u32)) {
348503
let position = offset_to_position(&Rope::from_str(source), offset, source);
349504
assert_eq!(position.line, expected.0);

0 commit comments

Comments
 (0)