This repository was archived by the owner on Aug 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 651
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
8b5b6ac
chore: Run `just new-lintrule`
nikeee 7c2d66c
feat: Implement noStaticOnlyClass
nikeee ea6f261
chore: just codegen-linter
nikeee 138b569
chore: `just promote-rule noStaticOnlyClass style`
nikeee bc038e0
chore: Add basic tests
nikeee a06e0af
chore: Apply some review fixes
nikeee 575bbfd
chore: Move rule to nursery
nikeee 9574ee0
chore: Move rule to analysis nursery
nikeee acc06ed
chore: Use tabs
nikeee bb44508
chore: Remove TODO
nikeee 91d1802
chore: Allow empty classes
nikeee 64d9537
chore: just codegen
nikeee 2e96012
chore: Adjust wording
nikeee f012fce
chore: Fix wording
nikeee ebc2d55
chore: `.iter_into()` -> `.iter()`, `is_empty()`
nikeee e7e170d
chore: Mark entire class declaration
nikeee f98d225
chore: Add some docs
nikeee 36c6adf
chore: Adjust wording 'field' -> 'member'
nikeee 39729a8
chore: Add macro for less DRY
nikeee 0e0b7ef
chore: Use is_empty
nikeee d6744cf
Merge branch 'main' into feature/noStaticOnlyClass
nikeee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
196 changes: 196 additions & 0 deletions
196
crates/rome_js_analyze/src/analyzers/nursery/no_static_only_class.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
use rome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; | ||
use rome_console::markup; | ||
use rome_js_syntax::{ | ||
AnyJsClass, AnyJsClassMember, JsGetterClassMember, JsMethodClassMember, JsPropertyClassMember, | ||
JsSetterClassMember, TsGetterSignatureClassMember, TsIndexSignatureClassMember, | ||
TsInitializedPropertySignatureClassMember, TsMethodSignatureClassMember, | ||
TsPropertySignatureClassMember, TsSetterSignatureClassMember, | ||
}; | ||
use rome_rowan::{AstNode, AstNodeList}; | ||
|
||
declare_rule! { | ||
/// This rule reports when a class has no non-static members, such as for a class used exclusively as a static namespace. | ||
/// | ||
/// Users who come from a [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) paradigm may wrap their utility functions in an extra class, | ||
/// instead of putting them at the top level of an ECMAScript module. Doing so is generally unnecessary in JavaScript and TypeScript projects. | ||
/// | ||
/// - Wrapper classes add extra cognitive complexity to code without adding any structural improvements | ||
/// - Whatever would be put on them, such as utility functions, are already organized by virtue of being in a module. | ||
/// - As an alternative, you can import * as ... the module to get all of them in a single object. | ||
/// - IDEs can't provide as good suggestions for static class or namespace imported properties when you start typing property names | ||
/// - It's more difficult to statically analyze code for unused variables, etc. when they're all on the class (see: Finding dead code (and dead types) in TypeScript). | ||
/// | ||
/// Source: https://typescript-eslint.io/rules/no-extraneous-class | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// class X { | ||
/// static foo = false; | ||
/// static bar() {}; | ||
/// } | ||
/// ``` | ||
/// ```js,expect_diagnostic | ||
/// class StaticConstants { | ||
/// static readonly version = 42; | ||
/// | ||
/// static isProduction() { | ||
/// return process.env.NODE_ENV === 'production'; | ||
/// } | ||
/// } | ||
/// ``` | ||
/// | ||
/// ## Valid | ||
/// | ||
/// ```js | ||
/// const X = { | ||
/// foo: false, | ||
/// bar() {} | ||
/// }; | ||
/// ``` | ||
/// ```js | ||
/// export const version = 42; | ||
/// | ||
/// export function isProduction() { | ||
/// return process.env.NODE_ENV === 'production'; | ||
/// } | ||
/// | ||
/// function logHelloWorld() { | ||
/// console.log('Hello, world!'); | ||
/// } | ||
/// ``` | ||
/// ```js | ||
/// class Empty {} | ||
/// ``` | ||
/// | ||
/// ## Notes on Mutating Variables | ||
/// One case you need to be careful of is exporting mutable variables. While class properties can be mutated externally, exported variables are always constant. This means that importers can only ever read the first value they are assigned and cannot write to the variables. | ||
/// | ||
/// Needing to write to an exported variable is very rare and is generally considered a code smell. If you do need it you can accomplish it using getter and setter functions: | ||
/// ```js,expect_diagnostic | ||
/// export class Utilities { | ||
/// static mutableCount = 1; | ||
/// static incrementCount() { | ||
/// Utilities.mutableCount += 1; | ||
/// } | ||
/// } | ||
/// ``` | ||
/// | ||
/// Do this instead: | ||
/// ```js | ||
/// let mutableCount = 1; | ||
/// | ||
/// export function getMutableCount() { | ||
/// return mutableField; | ||
/// } | ||
/// | ||
/// export function incrementCount() { | ||
/// mutableField += 1; | ||
/// } | ||
/// ``` | ||
pub(crate) NoStaticOnlyClass { | ||
version: "next", | ||
name: "noStaticOnlyClass", | ||
recommended: true, | ||
} | ||
} | ||
|
||
trait HasStaticModifier { | ||
fn has_static_modifier(&self) -> bool; | ||
} | ||
|
||
macro_rules! impl_has_static_modifiers { | ||
($type:ty) => { | ||
impl HasStaticModifier for $type { | ||
fn has_static_modifier(&self) -> bool { | ||
self.modifiers() | ||
.iter() | ||
.any(|modifier| modifier.as_js_static_modifier().is_some()) | ||
} | ||
} | ||
}; | ||
} | ||
|
||
impl_has_static_modifiers!(JsGetterClassMember); | ||
impl_has_static_modifiers!(JsSetterClassMember); | ||
impl_has_static_modifiers!(JsMethodClassMember); | ||
impl_has_static_modifiers!(JsPropertyClassMember); | ||
impl_has_static_modifiers!(TsGetterSignatureClassMember); | ||
impl_has_static_modifiers!(TsIndexSignatureClassMember); | ||
impl_has_static_modifiers!(TsInitializedPropertySignatureClassMember); | ||
impl_has_static_modifiers!(TsMethodSignatureClassMember); | ||
impl_has_static_modifiers!(TsPropertySignatureClassMember); | ||
impl_has_static_modifiers!(TsSetterSignatureClassMember); | ||
|
||
impl Rule for NoStaticOnlyClass { | ||
type Query = Ast<AnyJsClass>; | ||
type State = (); | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let class_declaration = ctx.query(); | ||
|
||
// If the class is empty, we can't be sure that the user can use a module-based approach instead | ||
// Also, the user might be in the process of writing a class, so we won't warn them yet | ||
if class_declaration.members().is_empty() { | ||
return None; | ||
} | ||
|
||
// If the class has decorators, we can't be sure that the user can use a module-based approach instead | ||
if !class_declaration.decorators().is_empty() { | ||
return None; | ||
} | ||
|
||
let all_members_static = class_declaration | ||
.members() | ||
.iter() | ||
.filter_map(|member| match member { | ||
AnyJsClassMember::JsBogusMember(_) => None, | ||
AnyJsClassMember::JsEmptyClassMember(_) => None, | ||
AnyJsClassMember::JsConstructorClassMember(_) => Some(false), // See GH#4482: Constructors are not regarded as static | ||
AnyJsClassMember::TsConstructorSignatureClassMember(_) => Some(false), // See GH#4482: Constructors are not regarded as static | ||
AnyJsClassMember::JsGetterClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::JsMethodClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::JsPropertyClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::JsSetterClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::JsStaticInitializationBlockClassMember(_) => Some(true), // See GH#4482: Static initialization blocks are regarded as static | ||
AnyJsClassMember::TsGetterSignatureClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::TsIndexSignatureClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::TsInitializedPropertySignatureClassMember(m) => { | ||
Some(m.has_static_modifier()) | ||
} | ||
AnyJsClassMember::TsMethodSignatureClassMember(m) => Some(m.has_static_modifier()), | ||
AnyJsClassMember::TsPropertySignatureClassMember(m) => { | ||
Some(m.has_static_modifier()) | ||
} | ||
AnyJsClassMember::TsSetterSignatureClassMember(m) => Some(m.has_static_modifier()), | ||
}) | ||
.all(|is_static| is_static); | ||
|
||
if all_members_static { | ||
Some(()) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> { | ||
let class_declaration = ctx.query(); | ||
|
||
Some( | ||
RuleDiagnostic::new( | ||
rule_category!(), | ||
class_declaration.syntax().text_trimmed_range(), | ||
markup! { | ||
"Avoid classes that contain only static members." | ||
}, | ||
) | ||
.note(markup! { | ||
"Prefer using simple functions instead of classes with only static members." | ||
}), | ||
) | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
crates/rome_js_analyze/tests/specs/nursery/noStaticOnlyClass/invalid.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
class X0 { | ||
static foo = false; | ||
static bar() { } | ||
} | ||
|
||
class X1 { | ||
static #foo = false; | ||
static bar() {} | ||
} | ||
|
||
const X2 = class { | ||
static foo = false; | ||
}; | ||
|
||
const X3 = class A { | ||
static foo = false; | ||
}; | ||
|
||
export default class { | ||
static foo = false; | ||
} | ||
|
||
class StaticConstants0 { | ||
static readonly version = 42; | ||
|
||
static isProduction() { | ||
return Math.random() > 0.5; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.