-
-
Notifications
You must be signed in to change notification settings - Fork 406
Add prefer-class-fields
rule
#2512
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
Merged
sindresorhus
merged 54 commits into
sindresorhus:main
from
FRSgit:feat/add-prefer-class-fields
Jun 14, 2025
Merged
Changes from 51 commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
76519ef
feat: add `prefer-class-fields` rule
FRSgit 0c33fa7
fix: handle edge case of constructor without body
FRSgit cd3149a
Update prefer-class-fields.md
sindresorhus 04ce366
Update prefer-class-fields.js
sindresorhus 2dba3a3
perf: iterate over original array instead of copying it over and reve…
FRSgit 78ca6ff
chore: handle only simple assignments
FRSgit 4136f2f
lint: fix
FRSgit 3e632b8
chore: spaces to tabs
FRSgit 9ae80ff
chore: add dynamic field names to valid test cases
FRSgit 621041e
feat: support strings and template strings
FRSgit bd47b24
chore: replace existing field if present
FRSgit f7989a4
chore: lint fix
FRSgit ed45d3d
feat: handle only non-computed properties
FRSgit faf6c2c
chore: stop static analysis on unsupported cases
FRSgit d77f630
chore: add missing semicolon
FRSgit 806ddef
docs: update docs/rules/prefer-class-fields.md
FRSgit 34f55d1
docs: update rule description
FRSgit 4d412a5
Update prefer-class-fields.md
sindresorhus ad13ba7
Update prefer-class-fields.md
sindresorhus 980bc42
Update prefer-class-fields.js
sindresorhus a691cf6
chore: update rules/prefer-class-fields.js
FRSgit 1bbd138
chore: change examples style
FRSgit 0386602
chore: rename test files
FRSgit 4fee503
chore: run fix:eslint docs (works only on node 22)
FRSgit f65e1fa
Update prefer-class-fields.md
sindresorhus 8322172
chore: rewrite autofix with side-effects to suggestion
FRSgit ec0d683
chore: add EmptyStatement as whitelisted node preceding this assignment
FRSgit a0f95f4
chore: fixup issue raised by unnamed class expressions
FRSgit d17f310
chore: post cr changes
FRSgit 6ee11bd
chore: lint fixes
FRSgit 2ad41e6
chore: lint fix
FRSgit 4e8f8a0
chore: add eslint docs
35e57d3
chore: update snapshots
FRSgit 197078e
chore: post cr changes with finding MethodDefinition constructor
FRSgit 2950c65
Fix snapshot
fisker 37a216e
Simplify logic
fisker fb6b239
Fix logic
fisker ab1362d
Rewrite tests
fisker 84a9c82
Safer
fisker 887aac6
Simplify
fisker ef80c7f
Linting
fisker 788978c
Linting
fisker dc28bcf
chore: support private fields
FRSgit ceace46
chore: update snapshots
FRSgit cbdaae6
chore: post rebase fixes
FRSgit fcb0a3a
Remove unused message data
fisker 8b7a2c3
Inline `addClassFieldDeclaration` to avoid ESLint warning
fisker bb7388c
Use one fix function
fisker 6b6cf06
More tests for fix
fisker d8a2aa8
Fix logic
fisker 5bd7ab4
Still incorrect
fisker 05528d3
One more test
fisker 570a881
Make sure we are using token instead of text
fisker f2f5b0f
Update prefer-class-fields.md
sindresorhus 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# Prefer class field declarations over `this` assignments in constructors | ||
|
||
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config). | ||
|
||
🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). | ||
|
||
<!-- end auto-generated rule header --> | ||
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` --> | ||
|
||
This rule enforces the use of class field declarations for static values, instead of assigning them in constructors using `this`. | ||
|
||
> To avoid leaving empty constructors after autofixing, use the [`no-useless-constructor` rule](https://eslint.org/docs/latest/rules/no-useless-constructor). | ||
|
||
## Examples | ||
|
||
```js | ||
// ❌ | ||
class Foo { | ||
constructor() { | ||
this.foo = 'foo'; | ||
} | ||
} | ||
|
||
// ✅ | ||
class Foo { | ||
foo = 'foo'; | ||
} | ||
``` | ||
|
||
```js | ||
// ❌ | ||
class MyError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = 'MyError'; | ||
} | ||
} | ||
|
||
// ✅ | ||
class MyError extends Error { | ||
name = 'MyError' | ||
} | ||
``` | ||
|
||
```js | ||
// ❌ | ||
class Foo { | ||
foo = 'foo'; | ||
constructor() { | ||
this.foo = 'bar'; | ||
} | ||
} | ||
|
||
// ✅ | ||
class Foo { | ||
foo = 'bar'; | ||
} | ||
``` | ||
|
||
```js | ||
// ❌ | ||
class Foo { | ||
#foo = 'foo'; | ||
constructor() { | ||
this.#foo = 'bar'; | ||
} | ||
} | ||
|
||
// ✅ | ||
class Foo { | ||
#foo = 'bar'; | ||
} | ||
``` |
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
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
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,147 @@ | ||
import getIndentString from './utils/get-indent-string.js'; | ||
|
||
const MESSAGE_ID_ERROR = 'prefer-class-fields/error'; | ||
const MESSAGE_ID_SUGGESTION = 'prefer-class-fields/suggestion'; | ||
const messages = { | ||
[MESSAGE_ID_ERROR]: | ||
'Prefer class field declaration over `this` assignment in constructor for static values.', | ||
[MESSAGE_ID_SUGGESTION]: | ||
'Encountered same-named class field declaration and `this` assignment in constructor. Replace the class field declaration with the value from `this` assignment.', | ||
}; | ||
|
||
/** | ||
@param {import('eslint').Rule.Node} node | ||
@param {import('eslint').Rule.RuleContext['sourceCode']} sourceCode | ||
@param {import('eslint').Rule.RuleFixer} fixer | ||
*/ | ||
const removeFieldAssignment = (node, sourceCode, fixer) => { | ||
const {line} = sourceCode.getLoc(node).start; | ||
const nodeText = sourceCode.getText(node); | ||
const lineText = sourceCode.lines[line - 1]; | ||
const isOnlyNodeOnLine = lineText.trim() === nodeText; | ||
|
||
return isOnlyNodeOnLine | ||
? fixer.removeRange([ | ||
sourceCode.getIndexFromLoc({line, column: 0}), | ||
sourceCode.getIndexFromLoc({line: line + 1, column: 0}), | ||
]) | ||
: fixer.remove(node); | ||
}; | ||
|
||
/** | ||
@type {import('eslint').Rule.RuleModule['create']} | ||
*/ | ||
const create = context => { | ||
const {sourceCode} = context; | ||
|
||
return { | ||
ClassBody(classBody) { | ||
const constructor = classBody.body.find(node => | ||
node.kind === 'constructor' | ||
&& !node.computed | ||
&& !node.static | ||
&& node.type === 'MethodDefinition' | ||
&& node.value.type === 'FunctionExpression', | ||
); | ||
|
||
if (!constructor) { | ||
return; | ||
} | ||
|
||
const node = constructor.value.body.body.find(node => node.type !== 'EmptyStatement'); | ||
|
||
if (!( | ||
node?.type === 'ExpressionStatement' | ||
&& node.expression.type === 'AssignmentExpression' | ||
&& node.expression.operator === '=' | ||
&& node.expression.left.type === 'MemberExpression' | ||
&& node.expression.left.object.type === 'ThisExpression' | ||
&& !node.expression.left.computed | ||
&& ['Identifier', 'PrivateIdentifier'].includes(node.expression.left.property.type) | ||
&& node.expression.right.type === 'Literal' | ||
)) { | ||
return; | ||
} | ||
|
||
const propertyName = node.expression.left.property.name; | ||
const propertyValue = node.expression.right.raw; | ||
const propertyType = node.expression.left.property.type; | ||
const existingProperty = classBody.body.find(node => | ||
node.type === 'PropertyDefinition' | ||
&& !node.computed | ||
&& !node.static | ||
&& node.key.type === propertyType | ||
&& node.key.name === propertyName, | ||
); | ||
|
||
const problem = { | ||
node, | ||
messageId: MESSAGE_ID_ERROR, | ||
}; | ||
|
||
/** | ||
@param {import('eslint').Rule.RuleFixer} fixer | ||
*/ | ||
function * fix(fixer) { | ||
yield removeFieldAssignment(node, sourceCode, fixer); | ||
|
||
if (existingProperty) { | ||
yield existingProperty.value | ||
? fixer.replaceText(existingProperty.value, propertyValue) | ||
: fixer.insertTextAfter(existingProperty.key, ` = ${propertyValue}`); | ||
return; | ||
} | ||
|
||
const closingBrace = sourceCode.getLastToken(classBody); | ||
const indent = getIndentString(constructor, sourceCode); | ||
|
||
let text = `${indent}${propertyName} = ${propertyValue};\n`; | ||
|
||
const characterBefore = sourceCode.getText()[sourceCode.getRange(closingBrace)[0] - 1]; | ||
if (characterBefore !== '\n') { | ||
text = `\n${text}`; | ||
} | ||
|
||
const lastProperty = classBody.body.at(-1); | ||
if ( | ||
lastProperty.type === 'PropertyDefinition' | ||
&& sourceCode.getLastToken(lastProperty).value !== ';' | ||
) { | ||
text = `;${text}`; | ||
} | ||
|
||
yield fixer.insertTextBefore(closingBrace, text); | ||
} | ||
|
||
if (existingProperty?.value) { | ||
problem.suggest = [ | ||
{ | ||
messageId: MESSAGE_ID_SUGGESTION, | ||
fix, | ||
}, | ||
]; | ||
return problem; | ||
} | ||
|
||
problem.fix = fix; | ||
return problem; | ||
}, | ||
}; | ||
}; | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
const config = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Prefer class field declarations over `this` assignments in constructors.', | ||
recommended: true, | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: true, | ||
messages, | ||
}, | ||
}; | ||
|
||
export default config; |
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
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
Oops, something went wrong.
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.