Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

seeintea
Copy link
Contributor

Summary

Resolves #4356

add useReadonlyClassProperties rule

add options checkAllProperties

Test Plan

Added tests and snapshots

@github-actions github-actions bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Nov 11, 2024
Copy link

codspeed-hq bot commented Nov 11, 2024

CodSpeed Performance Report

Merging #4507 will not alter performance

Comparing seeintea:feat/use_readonly_class_properties (9f24fe8) with main (d319d40)

Summary

✅ 95 untouched benchmarks

@github-actions github-actions bot added the A-CLI Area: CLI label Nov 11, 2024
Copy link
Contributor

@arendjr arendjr left a 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 😅

Comment on lines +29 to +30
/// This rule reports on private members are marked as readonly
/// if they're never modified outside of the constructor.
Copy link
Contributor

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:

Suggested change
/// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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
Copy link
Contributor

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.

Comment on lines +180 to +184
if eligible {
return Some(property_parameter.into());
}

None
Copy link
Contributor

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:

Suggested change
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 => {
Copy link
Contributor

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?

Copy link
Contributor Author

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 🌙)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, great!

@seeintea
Copy link
Contributor Author

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 😅

I just tested typescript-eslint

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 this.numbers.push(1); need use ReadonlyArray

private numbers: ReadonlyArray<number>;
// illegal operation
this.numbers.push(1); 

fn get_constructor_eligible_params(
class_declaration: &JsClassDeclaration,
only_private: bool,
) -> FxHashSet<AnyClassProperties> {
Copy link
Member

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?

Suggested change
) -> FxHashSet<AnyClassProperties> {
) -> Vec<AnyClassProperties> {

Comment on lines +413 to +415
.name_token()
.ok()?
.text_range(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.name_token()
.ok()?
.text_range(),
.range(),

}
}

pub fn into_syntax(&self) -> SyntaxElement<JsLanguage> {
Copy link
Member

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;

Conaclos
Conaclos previously approved these changes Nov 11, 2024
fn get_eligible_properties(
class_declaration: &JsClassDeclaration,
only_private: bool,
) -> FxHashSet<AnyClassProperties> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
) -> FxHashSet<AnyClassProperties> {
) -> Vec<AnyClassProperties> {

Copy link
Contributor Author

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

@Conaclos Conaclos self-requested a review November 11, 2024 17:59
@Conaclos Conaclos dismissed their stale review November 11, 2024 18:00

This was an error

this.#modifiedLaterPrivateField = 'mutated';
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Comment on lines +6 to +26
```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';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
```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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

📎 Implement useReadonlyClassProperties - typescript-eslint/prefer-readonly
4 participants