-
-
Notifications
You must be signed in to change notification settings - Fork 508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lint): add useReadonlyClassProperties
rule
#4507
base: main
Are you sure you want to change the base?
feat(lint): add useReadonlyClassProperties
rule
#4507
Conversation
CodSpeed Performance ReportMerging #4507 will not alter performanceComparing Summary
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
Main question: How do TypeScript's semantics affect interior mutability again? For instance, this member:
private readonly numbers: Array<number>;
Does this protect only against reassignment of numbers
? Or is this.numbers.push(1);
off-limits too now? I know there's the ReadonlyArray
type in TS, but I always forget if it gets implied by the readonly
keyword or not 😅
/// This rule reports on private members are marked as readonly | ||
/// if they're never modified outside of the constructor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean something like this:
/// This rule reports on private members are marked as readonly | |
/// if they're never modified outside of the constructor. | |
/// This rule reports on private members and marks them as `readonly` | |
/// if they're never modified outside of the constructor. |
/// | ||
/// ### checkAllProperties | ||
/// | ||
/// Check on all properties (`public` and `protected` properties). Default: false. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Check on all properties (`public` and `protected` properties). Default: false. | |
/// Check on all properties (`public` and `protected` properties). Default: `false`. |
/// This rule reports on private members are marked as readonly | ||
/// if they're never modified outside of the constructor. | ||
/// | ||
/// ## Examples |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally provide two sections of examples: ### Invalid
and ### Valid
(in that order). This helps people to understand the scope of the rule.
if eligible { | ||
return Some(property_parameter.into()); | ||
} | ||
|
||
None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think this is a little more idiomatic in Rust:
if eligible { | |
return Some(property_parameter.into()); | |
} | |
None | |
eligible.then(|| property_parameter.into()) |
WalkEvent::Enter(syntax_node) => match syntax_node.kind() { | ||
JsSyntaxKind::JS_CONSTRUCTOR_CLASS_MEMBER => constructor_member = true, | ||
JsSyntaxKind::JS_METHOD_CLASS_MEMBER => constructor_member = false, | ||
JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you may want to cover other mutating expressions as well, such as the +=
operator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+=
operator is also of JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION
I'll try to test more types tomorrow (It's nighttime here for me 🌙)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, great!
I just tested It only seems to care if the property is reassignment or not private numbers: Array<number>;
// It will still prompt [Member 'number' is never reassigned; mark it as `readonly`.]
this.number.push(1);
// but it won't be prompted.
this.number = [1]; So I think he only detects if a memory address has been modified. If want to disable private numbers: ReadonlyArray<number>;
// illegal operation
this.numbers.push(1); |
fn get_constructor_eligible_params( | ||
class_declaration: &JsClassDeclaration, | ||
only_private: bool, | ||
) -> FxHashSet<AnyClassProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use a Vec
?
) -> FxHashSet<AnyClassProperties> { | |
) -> Vec<AnyClassProperties> { |
.name_token() | ||
.ok()? | ||
.text_range(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.name_token() | |
.ok()? | |
.text_range(), | |
.range(), |
} | ||
} | ||
|
||
pub fn into_syntax(&self) -> SyntaxElement<JsLanguage> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you declared AnyClassProperties
as an AST node union, into_syntax()
or (syntax().clone()
) should be available it. To get access to these methods, you need to import AstNode
:
use biome_rowan::AstNode;
fn get_eligible_properties( | ||
class_declaration: &JsClassDeclaration, | ||
only_private: bool, | ||
) -> FxHashSet<AnyClassProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
) -> FxHashSet<AnyClassProperties> { | |
) -> Vec<AnyClassProperties> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll fix them, after I've finished cover other mutating expressions JsSyntaxKind
this.#modifiedLaterPrivateField = 'mutated'; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
```ts | ||
class ValidContainer { | ||
// Public members might be modified externally | ||
public publicMember: boolean; | ||
|
||
// Protected members might be modified by child classes | ||
protected protectedMember: number; | ||
|
||
// This is modified later on by the class | ||
private modifiedLater = 'unchanged'; | ||
|
||
public mutate() { | ||
this.modifiedLater = 'mutated'; | ||
} | ||
|
||
// This is modified later on by the class | ||
#modifiedLaterPrivateField = 'unchanged'; | ||
|
||
public mutatePrivateField() { | ||
this.#modifiedLaterPrivateField = 'mutated'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
```ts | |
class ValidContainer { | |
// Public members might be modified externally | |
public publicMember: boolean; | |
// Protected members might be modified by child classes | |
protected protectedMember: number; | |
// This is modified later on by the class | |
private modifiedLater = 'unchanged'; | |
public mutate() { | |
this.modifiedLater = 'mutated'; | |
} | |
// This is modified later on by the class | |
#modifiedLaterPrivateField = 'unchanged'; | |
public mutatePrivateField() { | |
this.#modifiedLaterPrivateField = 'mutated'; | |
} | |
```ts | |
class ValidContainer { | |
// Public members might be modified externally | |
public publicMember: boolean; | |
// Protected members might be modified by child classes | |
protected protectedMember: number; | |
// // Holds the initial state and updated by the `mutate` method | |
private modifiedLater = 'unchanged'; | |
public mutate() { | |
this.modifiedLater = 'mutated'; | |
} | |
// Holds the initial state and updated by the `mutatePrivateField` method | |
#modifiedLaterPrivateField = 'unchanged'; | |
public mutatePrivateField() { | |
this.#modifiedLaterPrivateField = 'mutated'; | |
} |
imho: the comment This is modified later on by the class
could be improved with a more precise and clear description of the field's purpose and behavior. how do you think?
Summary
Resolves #4356
add
useReadonlyClassProperties
ruleadd options
checkAllProperties
Test Plan
Added tests and snapshots