fix(linter/eslint/id-length): exempt TS interface/type-literal members with never (#26310)

## Summary

The `id-length` lint rule lets users opt out of checking property name
lengths via a `properties: "never"` setting. This exemption worked
correctly for plain JavaScript object properties, but was silently
ignored for TypeScript interface members and object type literal
members. As a result, users who explicitly disabled property length
checks would still see false-positive warnings on their TypeScript type
definitions.

## Why

TypeScript interfaces and type literals describe object shapes using
their own dedicated syntax, distinct from JavaScript object literals.
The rule's internal logic only recognized the JavaScript form when
deciding whether to skip the length check, so the TypeScript equivalents
were treated as ordinary identifiers instead of property names, even
though users intended the same exemption to apply to both.

## Changes

- The rule now recognizes TypeScript interface and type literal members
as property names, so the `properties: "never"` setting applies to them
consistently with plain JavaScript object properties.
- Added test coverage confirming both the exempted and non-exempted
behavior for TypeScript interface and type literal members.

## Test plan

- Added passing test cases showing the exemption now applies to
TypeScript interface and type literal members.
- Added failing test cases confirming the rule still flags these members
under its default settings.
- Verified the fix manually against the exact reproduction case reported
in the issue, confirming the false-positive warning no longer occurs.
- Full linter test suite passes with no regressions.

## AI disclosure

Per the repo's AI usage policy: this fix was drafted with AI assistance
(Claude), and reviewed, tested, and understood by me before submission.
The linked issue itself was also AI-drafted and reviewed by me.

Resolves #26309

---------

Signed-off-by: Cameron <cameron.clark@hey.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
This commit is contained in:
Pearce RopionandCameron authored and GitHub committed 2026-09-04 09:54:59 +01:00
1 parent 0f4af5d7df
commit 253cdb2f46
2 files changed
+47 -13

No files matched your search

+23 -13
View File
@@ -332,6 +332,8 @@ impl IdLength {
}
}
AstKind::ObjectProperty(_)
| AstKind::TSPropertySignature(_)
| AstKind::TSMethodSignature(_)
if self.properties == AlwaysNever::Never => {
return;
}
@@ -598,19 +600,23 @@ fn test() {
"export { default } from 'foo.json' with { type: 'json' }",
Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
), // { "ecmaVersion": 2025 },
// TODO:
// (
// "import('foo.json', { with: { type: 'json' } })",
// Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
// ), // { "ecmaVersion": 2025 },
// (
// "import('foo.json', { 'with': { type: 'json' } })",
// Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
// ), // { "ecmaVersion": 2025 },
// (
// "import('foo.json', { with: { type } })",
// Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
// ), // { "ecmaVersion": 2025 }
("interface Foo { a: string; }", Some(serde_json::json!([{ "properties": "never" }]))),
("interface Foo { a(): void; }", Some(serde_json::json!([{ "properties": "never" }]))),
("type Foo = { a: string };", Some(serde_json::json!([{ "properties": "never" }]))),
("type Foo = { a(): void };", Some(serde_json::json!([{ "properties": "never" }]))),
// TODO:
// (
// "import('foo.json', { with: { type: 'json' } })",
// Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
// ), // { "ecmaVersion": 2025 },
// (
// "import('foo.json', { 'with': { type: 'json' } })",
// Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
// ), // { "ecmaVersion": 2025 },
// (
// "import('foo.json', { with: { type } })",
// Some(serde_json::json!([{ "min": 1, "max": 3, "properties": "always" }])),
// ), // { "ecmaVersion": 2025 }
];
let fail = vec![
@@ -712,6 +718,10 @@ fn test() {
("var { 𐌘 } = {};", None), // { "ecmaVersion": 6, },
("var { prop: 𐌘} = {};", None), // { "ecmaVersion": 6, },
("({ prop: obj.𐌘 } = {});", None), // { "ecmaVersion": 6, }
("interface Foo { a: string; }", None),
("interface Foo { a(): void; }", None),
("type Foo = { a: string };", None),
("type Foo = { a(): void };", None),
];
Tester::new(IdLength::NAME, IdLength::PLUGIN, pass, fail).test_and_snapshot();
@@ -553,3 +553,27 @@ source: crates/oxc_linter/src/tester.rs
1 │ ({ prop: obj.𐌘 } = {});
· ─
╰────
⚠ eslint(id-length): Identifier name is too short (< 2).
╭─[id_length.tsx:1:17]
1 │ interface Foo { a: string; }
· ─
╰────
⚠ eslint(id-length): Identifier name is too short (< 2).
╭─[id_length.tsx:1:17]
1 │ interface Foo { a(): void; }
· ─
╰────
⚠ eslint(id-length): Identifier name is too short (< 2).
╭─[id_length.tsx:1:14]
1 │ type Foo = { a: string };
· ─
╰────
⚠ eslint(id-length): Identifier name is too short (< 2).
╭─[id_length.tsx:1:14]
1 │ type Foo = { a(): void };
· ─
╰────