mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(linter/typescript-eslint): implement suggestion for class-literal-property-style rule (#24766)
this PR implements suggestion for `typescript-eslint/class-literal-property-style` rule. Passes all (19) [upstream test cases](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/tests/rules/class-literal-property-style.test.ts) issue #2180
This commit is contained in:
1 parent
98f2ecbb10
commit
678bbb5f34
3 files changed
+502
-11
No files matched your search
@@ -5,7 +5,7 @@ use oxc_ast::{
|
||||
ast::{
|
||||
ArrowFunctionExpression, AssignmentExpression, AssignmentTarget, Class, ClassBody,
|
||||
ClassElement, Expression, Function, MethodDefinitionKind, PropertyDefinition, PropertyKey,
|
||||
Statement,
|
||||
Statement, TSAccessibility,
|
||||
},
|
||||
};
|
||||
use oxc_ast_visit::VisitJs;
|
||||
@@ -125,7 +125,7 @@ declare_oxc_lint!(
|
||||
ClassLiteralPropertyStyle,
|
||||
typescript,
|
||||
style,
|
||||
pending,
|
||||
conditional_suggestion,
|
||||
config = ClassLiteralPropertyStyleOption,
|
||||
version = "1.47.0",
|
||||
short_description = "Enforces a consistent style for exposing literal values on classes.",
|
||||
@@ -163,7 +163,33 @@ fn check_fields_mode<'a>(class_body: &ClassBody<'a>, ctx: &LintContext<'a>) {
|
||||
&& is_supported_literal(argument)
|
||||
&& !has_duplicate_setter(class_body, method)
|
||||
{
|
||||
ctx.diagnostic(prefer_field_style_diagnostic(method.key.span()));
|
||||
if !method.decorators.is_empty() {
|
||||
ctx.diagnostic(prefer_field_style_diagnostic(method.key.span()));
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.diagnostic_with_suggestion(
|
||||
prefer_field_style_diagnostic(method.key.span()),
|
||||
|fixer| {
|
||||
let mut replacement =
|
||||
print_member_modifiers(method.accessibility, method.r#static, "readonly");
|
||||
push_member_key(
|
||||
&mut replacement,
|
||||
fixer.source_range(method.key.span()),
|
||||
method.computed,
|
||||
);
|
||||
if let Some(return_type) = &method.value.return_type {
|
||||
replacement.push_str(fixer.source_range(return_type.span));
|
||||
}
|
||||
replacement.push_str(" = ");
|
||||
replacement.push_str(fixer.source_range(argument.span()));
|
||||
replacement.push(';');
|
||||
|
||||
fixer
|
||||
.replace(method.span, replacement)
|
||||
.with_message("Replace the literals with readonly fields.")
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +209,7 @@ fn check_getters_mode<'a>(class_body: &ClassBody<'a>, ctx: &LintContext<'a>) {
|
||||
|
||||
for element in &class_body.body {
|
||||
if let ClassElement::PropertyDefinition(property) = element
|
||||
&& is_literal_readonly_property(property)
|
||||
&& let Some(value) = literal_readonly_property_value(property)
|
||||
{
|
||||
if let Some(name) = property.key.name()
|
||||
&& excluded_properties.contains(&*name)
|
||||
@@ -191,11 +217,66 @@ fn check_getters_mode<'a>(class_body: &ClassBody<'a>, ctx: &LintContext<'a>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.diagnostic(prefer_getter_style_diagnostic(property.key.span()));
|
||||
if !property.decorators.is_empty() {
|
||||
ctx.diagnostic(prefer_getter_style_diagnostic(property.key.span()));
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.diagnostic_with_suggestion(
|
||||
prefer_getter_style_diagnostic(property.key.span()),
|
||||
|fixer| {
|
||||
let mut replacement =
|
||||
print_member_modifiers(property.accessibility, property.r#static, "get");
|
||||
push_member_key(
|
||||
&mut replacement,
|
||||
fixer.source_range(property.key.span()),
|
||||
property.computed,
|
||||
);
|
||||
replacement.push_str("()");
|
||||
if let Some(type_annotation) = &property.type_annotation {
|
||||
replacement.push_str(fixer.source_range(type_annotation.span));
|
||||
}
|
||||
replacement.push_str(" { return ");
|
||||
replacement.push_str(fixer.source_range(value.span()));
|
||||
replacement.push_str("; }");
|
||||
|
||||
fixer
|
||||
.replace(property.span, replacement)
|
||||
.with_message("Replace the literals with getters.")
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_member_modifiers(
|
||||
accessibility: Option<TSAccessibility>,
|
||||
is_static: bool,
|
||||
final_modifier: &str,
|
||||
) -> String {
|
||||
let mut text = String::new();
|
||||
if let Some(accessibility) = accessibility {
|
||||
text.push_str(accessibility.as_str());
|
||||
text.push(' ');
|
||||
}
|
||||
if is_static {
|
||||
text.push_str("static ");
|
||||
}
|
||||
text.push_str(final_modifier);
|
||||
text.push(' ');
|
||||
text
|
||||
}
|
||||
|
||||
fn push_member_key(text: &mut String, key: &str, computed: bool) {
|
||||
if computed {
|
||||
text.push('[');
|
||||
}
|
||||
text.push_str(key);
|
||||
if computed {
|
||||
text.push(']');
|
||||
}
|
||||
}
|
||||
|
||||
fn has_duplicate_setter<'a>(
|
||||
class_body: &ClassBody<'a>,
|
||||
getter: &oxc_ast::ast::MethodDefinition<'a>,
|
||||
@@ -211,11 +292,14 @@ fn has_duplicate_setter<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn is_literal_readonly_property(property: &PropertyDefinition<'_>) -> bool {
|
||||
property.readonly
|
||||
&& !property.declare
|
||||
&& !property.r#override
|
||||
&& property.value.as_ref().is_some_and(is_supported_literal)
|
||||
fn literal_readonly_property_value<'a, 'b>(
|
||||
property: &'a PropertyDefinition<'b>,
|
||||
) -> Option<&'a Expression<'b>> {
|
||||
if !property.readonly || property.declare || property.r#override {
|
||||
return None;
|
||||
}
|
||||
|
||||
property.value.as_ref().filter(|value| is_supported_literal(value))
|
||||
}
|
||||
|
||||
fn is_supported_literal(expression: &Expression<'_>) -> bool {
|
||||
@@ -657,6 +741,16 @@ fn test() {
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
get foo(): string {
|
||||
return 'x';
|
||||
}
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
@@ -693,6 +787,14 @@ fn test() {
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
readonly n: 1 | 2 = 1;
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
@@ -826,6 +928,377 @@ fn test() {
|
||||
),
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
get p1() {
|
||||
return 'hello world';
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
get p1() {
|
||||
return `hello world`;
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
readonly p1 = `hello world`;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
static get p1() {
|
||||
return 'hello world';
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
static readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public static get foo() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public static readonly foo = 1;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
get foo(): string {
|
||||
return 'x';
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
readonly foo: string = 'x';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public get [myValue]() {
|
||||
return 'a literal value';
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public readonly [myValue] = 'a literal value';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public get [myValue]() {
|
||||
return 12345n;
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public readonly [myValue] = 12345n;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public readonly [myValue] = 'a literal value';
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public get [myValue]() { return 'a literal value'; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
get p1() { return 'hello world'; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
readonly n: 1 | 2 = 1;
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
get n(): 1 | 2 { return 1; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
readonly p1 = `hello world`;
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
get p1() { return `hello world`; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
static readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
static get p1() { return 'hello world'; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
protected get p1() {
|
||||
return 'hello world';
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
protected readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["fields"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
protected readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
protected get p1() { return 'hello world'; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public static get p1() {
|
||||
return 'hello world';
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public static readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public static readonly p1 = 'hello world';
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public static get p1() { return 'hello world'; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public get myValue() {
|
||||
return gql`
|
||||
{
|
||||
user(id: 5) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public readonly myValue = gql`
|
||||
{
|
||||
user(id: 5) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Mx {
|
||||
public readonly myValue = gql`
|
||||
{
|
||||
user(id: 5) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
",
|
||||
"
|
||||
class Mx {
|
||||
public get myValue() { return gql`
|
||||
{
|
||||
user(id: 5) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`; }
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class A {
|
||||
private readonly foo: string = 'bar';
|
||||
constructor(foo: string) {
|
||||
const bar = new (class {
|
||||
private readonly foo: string = 'baz';
|
||||
constructor() {
|
||||
this.foo = 'qux';
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class A {
|
||||
private get foo(): string { return 'bar'; }
|
||||
constructor(foo: string) {
|
||||
const bar = new (class {
|
||||
private readonly foo: string = 'baz';
|
||||
constructor() {
|
||||
this.foo = 'qux';
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class A {
|
||||
private readonly ['foo']: string = 'bar';
|
||||
constructor(foo: string) {
|
||||
const bar = new (class {
|
||||
private readonly foo: string = 'baz';
|
||||
constructor() {}
|
||||
})();
|
||||
|
||||
if (bar) {
|
||||
this.foo = 'baz';
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class A {
|
||||
private readonly ['foo']: string = 'bar';
|
||||
constructor(foo: string) {
|
||||
const bar = new (class {
|
||||
private get foo(): string { return 'baz'; }
|
||||
constructor() {}
|
||||
})();
|
||||
|
||||
if (bar) {
|
||||
this.foo = 'baz';
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
(
|
||||
"
|
||||
class A {
|
||||
private readonly foo: string = 'bar';
|
||||
constructor(foo: string) {
|
||||
function func() {
|
||||
this.foo = 'aa';
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
"
|
||||
class A {
|
||||
private get foo(): string { return 'bar'; }
|
||||
constructor(foo: string) {
|
||||
function func() {
|
||||
this.foo = 'aa';
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
Some(serde_json::json!(["getters"])),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(ClassLiteralPropertyStyle::NAME, ClassLiteralPropertyStyle::PLUGIN, pass, fail)
|
||||
.expect_fix(fix)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
@@ -38,6 +38,15 @@ source: crates/oxc_linter/src/tester.rs
|
||||
╰────
|
||||
help: Replace this getter with a readonly field initialized to the returned literal.
|
||||
|
||||
⚠ typescript(class-literal-property-style): Literals should be exposed using readonly fields.
|
||||
╭─[class_literal_property_style.tsx:3:19]
|
||||
2 │ class Mx {
|
||||
3 │ get foo(): string {
|
||||
· ───
|
||||
4 │ return 'x';
|
||||
╰────
|
||||
help: Replace this getter with a readonly field initialized to the returned literal.
|
||||
|
||||
⚠ typescript(class-literal-property-style): Literals should be exposed using readonly fields.
|
||||
╭─[class_literal_property_style.tsx:3:27]
|
||||
2 │ class Mx {
|
||||
@@ -77,6 +86,15 @@ source: crates/oxc_linter/src/tester.rs
|
||||
⚠ typescript(class-literal-property-style): Literals should be exposed using getters.
|
||||
╭─[class_literal_property_style.tsx:3:24]
|
||||
2 │ class Mx {
|
||||
3 │ readonly n: 1 | 2 = 1;
|
||||
· ─
|
||||
4 │ }
|
||||
╰────
|
||||
help: Replace this readonly literal field with a getter.
|
||||
|
||||
⚠ typescript(class-literal-property-style): Literals should be exposed using getters.
|
||||
╭─[class_literal_property_style.tsx:3:24]
|
||||
2 │ class Mx {
|
||||
3 │ readonly p1 = `hello world`;
|
||||
· ──
|
||||
4 │ }
|
||||
|
||||
@@ -2099,7 +2099,7 @@ category: "Style"
|
||||
version: "1.47.0"
|
||||
default: false
|
||||
type_aware: false
|
||||
fix: "pending"
|
||||
fix: "conditional_suggestion"
|
||||
upstream: "https://typescript-eslint.io/rules/class-literal-property-style/"
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user