fix(linter/eslint/one-var): keep declare when splitting declarations (#27081)

## Root cause

With `"never"`, `report_split`
(`crates/oxc_linter/src/rules/eslint/one_var.rs:524-551`) inserts a new
declaration prefix before every declarator after the first. The prefix
(`:537-538`) is only `export` plus the keyword, so `declare` is dropped
and the split-off declarators become runtime declarations. With oxlint
1.85.0, `--fix` gives:

| input | output |
| --- | --- |
| `declare var a: string, b: number;` | `declare var a: string; var b:
number;` |
| `export declare let a: string, b: number;` | `export declare let a:
string; export let b: number;` |
| `declare const a: string, b: number;` | `declare const a: string;
const b: number;` |

The first two still parse, but add a runtime `var b` / `export let b`
that the source did not have. The last one no longer parses (`Missing
initializer in const declaration`); in a debug build it trips the
fixer's syntax check.

## Fix

Add `declare ` to the prefix when `declaration.declare` is set, after
`export`, so each split-off declaration keeps the modifiers of the
original one. The join path already handles `declare` (`report_join`
reports without a fix since #25314); the split path only has to keep the
modifier. Declarations inside `declare namespace` / `declare global`
don't carry the flag, so they are split as before (TypeScript rejects
`declare` in an already ambient context).

Alternative: report without a fix when `declare` is set, like the join
path. I kept the fix because re-emitting the modifier is enough to make
the output equivalent to the input.

ESLint's fixer builds the same `export` + keyword prefix and has no
TypeScript handling, so there is no upstream behavior to match; this is
a correctness fix.

## Notes

AI-assisted: I used Claude Code while investigating; I reviewed, ran and
verified every change myself.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cheolhee LeeandClaude Opus 5.5 authored and GitHub committed 2026-09-28 09:07:20 +01:00
1 parent cec6571f03
commit 42dfbb5010
2 files changed
+68 -1

No files matched your search

+33 -1
View File
@@ -535,7 +535,9 @@ fn report_split(
ctx.diagnostic_with_fix(diagnostic, |fixer| {
let keyword = declaration.kind.as_str();
let exported = matches!(ctx.nodes().parent_kind(node.id()), AstKind::ExportDeclaration(_));
let prefix = if exported { format!("export {keyword} ") } else { format!("{keyword} ") };
let export = if exported { "export " } else { "" };
let declare = if declaration.declare { "declare " } else { "" };
let prefix = format!("{export}{declare}{keyword} ");
let mut fixes = fixer.new_fix_with_capacity((declaration.declarations.len() - 1) * 2);
for [left, right] in declaration.declarations.array_windows() {
let offset = ctx
@@ -1331,6 +1333,11 @@ fn test() {
Some(serde_json::json!(["always"])),
),
("declare const foo: number; const bar = 2;", Some(serde_json::json!(["always"]))),
("declare var a: string, b: number;", Some(serde_json::json!(["never"]))),
("declare const a: string, b: number;", Some(serde_json::json!(["never"]))),
("export declare let a: string, b: number;", Some(serde_json::json!(["never"]))),
("declare namespace N { var a, b; }", Some(serde_json::json!(["never"]))),
("declare global { var a: string, b: number; }", Some(serde_json::json!(["never"]))),
("if (foo) var x, y;", Some(serde_json::json!(["never"]))),
("if (foo) var x, y;", Some(serde_json::json!([{ "var": "never" }]))),
("if (foo) var x, y;", Some(serde_json::json!([{ "uninitialized": "never" }]))),
@@ -1899,6 +1906,31 @@ fn test() {
"declare const foo: number; const bar = 2;",
Some(serde_json::json!(["always"])),
),
(
"declare var a: string, b: number;",
"declare var a: string; declare var b: number;",
Some(serde_json::json!(["never"])),
),
(
"declare const a: string, b: number;",
"declare const a: string; declare const b: number;",
Some(serde_json::json!(["never"])),
),
(
"export declare let a: string, b: number;",
"export declare let a: string; export declare let b: number;",
Some(serde_json::json!(["never"])),
),
(
"declare namespace N { var a, b; }",
"declare namespace N { var a; var b; }",
Some(serde_json::json!(["never"])),
),
(
"declare global { var a: string, b: number; }",
"declare global { var a: string; var b: number; }",
Some(serde_json::json!(["never"])),
),
(
"class C { static { let x, y; } }",
"class C { static { let x; let y; } }",
@@ -972,6 +972,41 @@ source: crates/oxc_linter/src/tester.rs
· ──────────────
╰────
⚠ eslint(one-var): Split 'var' declarations into multiple statements.
╭─[one_var.tsx:1:1]
1 │ declare var a: string, b: number;
· ─────────────────────────────────
╰────
help: Split variable declarations
⚠ eslint(one-var): Split 'const' declarations into multiple statements.
╭─[one_var.tsx:1:1]
1 │ declare const a: string, b: number;
· ───────────────────────────────────
╰────
help: Split variable declarations
⚠ eslint(one-var): Split 'let' declarations into multiple statements.
╭─[one_var.tsx:1:8]
1 │ export declare let a: string, b: number;
· ─────────────────────────────────
╰────
help: Split variable declarations
⚠ eslint(one-var): Split 'var' declarations into multiple statements.
╭─[one_var.tsx:1:23]
1 │ declare namespace N { var a, b; }
· ─────────
╰────
help: Split variable declarations
⚠ eslint(one-var): Split 'var' declarations into multiple statements.
╭─[one_var.tsx:1:18]
1 │ declare global { var a: string, b: number; }
· ─────────────────────────
╰────
help: Split variable declarations
⚠ eslint(one-var): Split 'var' declarations into multiple statements.
╭─[one_var.tsx:1:10]
1 │ if (foo) var x, y;