@@ -5,10 +5,12 @@ use oxc_ast::{
5
5
use oxc_diagnostics:: OxcDiagnostic ;
6
6
use oxc_macros:: declare_oxc_lint;
7
7
use oxc_span:: Span ;
8
+ use schemars:: JsonSchema ;
8
9
9
10
use crate :: { AstNode , context:: LintContext , rule:: Rule } ;
10
11
11
- #[ derive( Debug , Default , Clone ) ]
12
+ #[ derive( Debug , Clone , Default , JsonSchema ) ]
13
+ #[ serde( renameAll = "camelCase" , default ) ]
12
14
pub struct NoExtraneousClass {
13
15
allow_constructor_only : bool ,
14
16
allow_empty : bool ,
@@ -19,26 +21,33 @@ pub struct NoExtraneousClass {
19
21
declare_oxc_lint ! (
20
22
/// ### What it does
21
23
///
22
- /// This rule reports when a class has no non-static members,
23
- /// such as for a class used exclusively as a static namespace.
24
- /// This rule also reports classes that have only a constructor and no fields.
25
- /// Those classes can generally be replaced with a standalone function.
24
+ /// This rule reports when a class has no non-static members, such as for a
25
+ /// class used exclusively as a static namespace. This rule also reports
26
+ /// classes that have only a constructor and no fields. Those classes can
27
+ /// generally be replaced with a standalone function.
26
28
///
27
29
/// ### Why is this bad?
28
30
///
29
- /// Users who come from a OOP paradigm may wrap their utility functions in an extra class,
30
- /// instead of putting them at the top level of an ECMAScript module.
31
- /// Doing so is generally unnecessary in JavaScript and TypeScript projects.
31
+ /// Users who come from a OOP paradigm may wrap their utility functions in
32
+ /// an extra class, instead of putting them at the top level of an
33
+ /// ECMAScript module. Doing so is generally unnecessary in JavaScript and
34
+ /// TypeScript projects.
32
35
///
33
- /// Wrapper classes add extra cognitive complexity to code without adding any structural improvements
36
+ /// * Wrapper classes add extra cognitive complexity to code without adding
37
+ /// any structural improvements
38
+ /// * Whatever would be put on them, such as utility functions, are already
39
+ /// organized by virtue of being in a module.
40
+ /// * As an alternative, you can `import * as ...` the module to get all of them
41
+ /// in a single object.
42
+ /// * IDEs can't provide as good suggestions for static class or namespace
43
+ /// imported properties when you start typing property names
44
+ /// * It's more difficult to statically analyze code for unused variables,
45
+ /// etc. when they're all on the class (see: [Finding dead code (and dead
46
+ /// types) in TypeScript](https://effectivetypescript.com/2020/10/20/tsprune/)).
34
47
///
35
- /// Whatever would be put on them, such as utility functions, are already organized by virtue of being in a module.
36
- ///
37
- /// As an alternative, you can import * as ... the module to get all of them in a single object.
38
- /// IDEs can't provide as good suggestions for static class or namespace imported properties when you start typing property names
39
- ///
40
- /// It's more difficult to statically analyze code for unused variables, etc.
41
- /// when they're all on the class (see: Finding dead code (and dead types) in TypeScript).
48
+ /// This rule also reports classes that have only a constructor and no
49
+ /// fields. Those classes can generally be replaced with a standalone
50
+ /// function.
42
51
///
43
52
/// ### Example
44
53
/// ```ts
@@ -63,16 +72,27 @@ declare_oxc_lint!(
63
72
suspicious
64
73
) ;
65
74
66
- fn empty_no_extraneous_class_diagnostic ( span : Span ) -> OxcDiagnostic {
67
- OxcDiagnostic :: warn ( "Unexpected empty class." ) . with_label ( span)
75
+ fn empty_class_diagnostic ( span : Span , has_decorators : bool ) -> OxcDiagnostic {
76
+ let diagnostic = OxcDiagnostic :: warn ( "Unexpected empty class." ) . with_label ( span) ;
77
+ if has_decorators {
78
+ diagnostic. with_help (
79
+ r#"Set "allowWithDecorator": true in your config to allow empty decorated classes"# ,
80
+ )
81
+ } else {
82
+ diagnostic
83
+ }
68
84
}
69
85
70
86
fn only_static_no_extraneous_class_diagnostic ( span : Span ) -> OxcDiagnostic {
71
- OxcDiagnostic :: warn ( "Unexpected class with only static properties." ) . with_label ( span)
87
+ OxcDiagnostic :: warn ( "Unexpected class with only static properties." )
88
+ . with_label ( span)
89
+ . with_help ( "Try using standalone functions instead of static methods" )
72
90
}
73
91
74
92
fn only_constructor_no_extraneous_class_diagnostic ( span : Span ) -> OxcDiagnostic {
75
- OxcDiagnostic :: warn ( "Unexpected class with only a constructor." ) . with_label ( span)
93
+ OxcDiagnostic :: warn ( "Unexpected class with only a constructor." )
94
+ . with_label ( span)
95
+ . with_help ( "Try replacing this class with a standalone function or deleting it entirely" )
76
96
}
77
97
78
98
impl Rule for NoExtraneousClass {
@@ -115,7 +135,19 @@ impl Rule for NoExtraneousClass {
115
135
match body. as_slice ( ) {
116
136
[ ] => {
117
137
if !self . allow_empty {
118
- ctx. diagnostic ( empty_no_extraneous_class_diagnostic ( class. span ) ) ;
138
+ let mut span = class. span ;
139
+ #[ expect( clippy:: checked_conversions, clippy:: cast_possible_truncation) ]
140
+ if let Some ( decorator) = class. decorators . last ( ) {
141
+ span = Span :: new ( decorator. span . end , span. end ) ;
142
+ // NOTE: there will always be a 'c' because of 'class' keyword.
143
+ let start = ctx. source_range ( span) . find ( 'c' ) . unwrap ( ) ;
144
+ // SAFETY: source files are guaranteed to be less than
145
+ // 2^32 characters, so conversion will never fail. Using
146
+ // unchecked assert here removes a useless bounds check.
147
+ unsafe { std:: hint:: assert_unchecked ( start <= u32:: MAX as usize ) } ;
148
+ span = span. shrink_left ( start as u32 ) ;
149
+ }
150
+ ctx. diagnostic ( empty_class_diagnostic ( span, !class. decorators . is_empty ( ) ) ) ;
119
151
}
120
152
}
121
153
[ ClassElement :: MethodDefinition ( constructor) ] if constructor. kind . is_constructor ( ) => {
@@ -138,6 +170,7 @@ impl Rule for NoExtraneousClass {
138
170
#[ test]
139
171
fn test ( ) {
140
172
use crate :: tester:: Tester ;
173
+ use serde_json:: json;
141
174
142
175
let pass = vec ! [
143
176
(
@@ -146,7 +179,7 @@ fn test() {
146
179
public prop = 1;
147
180
constructor() {}
148
181
}
149
- ",
182
+ ",
150
183
None ,
151
184
) ,
152
185
(
@@ -158,26 +191,12 @@ fn test() {
158
191
}
159
192
constructor() {}
160
193
}
161
- " ,
162
- None ,
163
- ) ,
164
- (
165
- "
166
- class Foo {
167
- constructor(public bar: string) {}
168
- }
169
- " ,
194
+ " ,
170
195
None ,
171
196
) ,
172
- ( "class Foo {}" , Some ( serde_json:: json!( [ { "allowEmpty" : true } ] ) ) ) ,
173
- (
174
- "
175
- class Foo {
176
- constructor() {}
177
- }
178
- " ,
179
- Some ( serde_json:: json!( [ { "allowConstructorOnly" : true } ] ) ) ,
180
- ) ,
197
+ ( "class Foo { constructor(public bar: string) {} }" , None ) ,
198
+ ( "class Foo {}" , Some ( json!( [ { "allowEmpty" : true } ] ) ) ) ,
199
+ ( "class Foo { constructor() {} }" , Some ( json!( [ { "allowConstructorOnly" : true } ] ) ) ) ,
181
200
(
182
201
"
183
202
export class Bar {
@@ -187,7 +206,7 @@ fn test() {
187
206
}
188
207
}
189
208
" ,
190
- Some ( serde_json :: json!( [ { "allowStaticOnly" : true } ] ) ) ,
209
+ Some ( json!( [ { "allowStaticOnly" : true } ] ) ) ,
191
210
) ,
192
211
(
193
212
"
@@ -196,16 +215,10 @@ fn test() {
196
215
return 'I am foo!';
197
216
}
198
217
}
199
- " ,
218
+ " ,
200
219
None ,
201
220
) ,
202
- (
203
- "
204
- @FooDecorator
205
- class Foo {}
206
- " ,
207
- Some ( serde_json:: json!( [ { "allowWithDecorator" : true } ] ) ) ,
208
- ) ,
221
+ ( "@FooDecorator class Foo {} " , Some ( json!( [ { "allowWithDecorator" : true } ] ) ) ) ,
209
222
(
210
223
"
211
224
@FooDecorator
@@ -216,25 +229,11 @@ fn test() {
216
229
});
217
230
}
218
231
}
219
- " ,
220
- Some ( serde_json:: json!( [ { "allowWithDecorator" : true } ] ) ) ,
221
- ) ,
222
- (
223
- "
224
- abstract class Foo {
225
- abstract property: string;
226
- }
227
- " ,
228
- None ,
229
- ) ,
230
- (
231
- "
232
- abstract class Foo {
233
- abstract method(): string;
234
- }
235
- " ,
236
- None ,
232
+ " ,
233
+ Some ( json!( [ { "allowWithDecorator" : true } ] ) ) ,
237
234
) ,
235
+ ( "abstract class Foo { abstract property: string; }" , None ) ,
236
+ ( "abstract class Foo { abstract method(): string; }" , None ) ,
238
237
] ;
239
238
240
239
let fail = vec ! [
@@ -258,14 +257,7 @@ fn test() {
258
257
" ,
259
258
None ,
260
259
) ,
261
- (
262
- "
263
- class Foo {
264
- constructor() {}
265
- }
266
- " ,
267
- None ,
268
- ) ,
260
+ ( "class Foo { constructor() {} }" , None ) ,
269
261
(
270
262
"
271
263
export class AClass {
@@ -280,20 +272,23 @@ fn test() {
280
272
" ,
281
273
None ,
282
274
) ,
275
+ ( "export default class { static hello() {} }" , None ) ,
283
276
(
284
277
"
285
- export default class {
286
- static hello() {}
287
- }
288
- " ,
289
- None ,
278
+ @FooDecorator
279
+ class Foo {}
280
+ " ,
281
+ Some ( json!( [ { "allowWithDecorator" : false } ] ) ) ,
290
282
) ,
291
283
(
292
284
"
293
- @FooDecorator
285
+ @FooDecorator({
286
+ wowThisDecoratorIsQuiteLarge: true,
287
+ itShouldNotBeIncludedIn: 'the diagnostic span',
288
+ })
294
289
class Foo {}
295
- ",
296
- Some ( serde_json :: json!( [ { "allowWithDecorator" : false } ] ) ) ,
290
+ ",
291
+ Some ( json!( [ { "allowWithDecorator" : false } ] ) ) ,
297
292
) ,
298
293
(
299
294
"
@@ -305,31 +300,12 @@ fn test() {
305
300
});
306
301
}
307
302
}
308
- " ,
309
- Some ( serde_json:: json!( [ { "allowWithDecorator" : false } ] ) ) ,
310
- ) ,
311
- (
312
- "
313
- abstract class Foo {}
314
- " ,
315
- None ,
316
- ) ,
317
- (
318
- "
319
- abstract class Foo {
320
- static property: string;
321
- }
322
- " ,
323
- None ,
324
- ) ,
325
- (
326
- "
327
- abstract class Foo {
328
- constructor() {}
329
- }
330
- " ,
331
- None ,
303
+ " ,
304
+ Some ( json!( [ { "allowWithDecorator" : false } ] ) ) ,
332
305
) ,
306
+ ( "abstract class Foo {}" , None ) ,
307
+ ( "abstract class Foo { static property: string; }" , None ) ,
308
+ ( "abstract class Foo { constructor() {} }" , None ) ,
333
309
] ;
334
310
335
311
Tester :: new ( NoExtraneousClass :: NAME , NoExtraneousClass :: PLUGIN , pass, fail) . test_and_snapshot ( ) ;
0 commit comments