-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add
class-literal-property-style
rule (#1582)
- Loading branch information
Showing
6 changed files
with
614 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
96 changes: 96 additions & 0 deletions
96
packages/eslint-plugin/docs/rules/class-literal-property-style.md
This file contains 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,96 @@ | ||
# Ensures that literals on classes are exposed in a consistent style (`class-literal-property-style`) | ||
|
||
When writing TypeScript applications, it's typically safe to store literal values on classes using fields with the `readonly` modifier to prevent them from being reassigned. | ||
When writing TypeScript libraries that could be used by Javascript users however, it's typically safer to expose these literals using `getter`s, since the `readonly` modifier is enforced at compile type. | ||
|
||
## Rule Details | ||
|
||
This rule aims to ensure that literals exposed by classes are done so consistently, in one of the two style described above. | ||
By default this rule prefers the `fields` style as it means JS doesn't have to setup & teardown a function closure. | ||
|
||
Note that this rule only checks for constant _literal_ values (string, template string, number, bigint, boolean, regexp, null). It does not check objects or arrays, because a readonly field behaves differently to a getter in those cases. It also does not check functions, as it is a common pattern to use readonly fields with arrow function values as auto-bound methods. | ||
This is because these types can be mutated and carry with them more complex implications about their usage. | ||
|
||
#### The `fields` style | ||
|
||
This style checks for any getter methods that return literal values, and requires them to be defined using fields with the `readonly` modifier instead. | ||
|
||
Examples of **correct** code with the `fields` style: | ||
|
||
```ts | ||
/* eslint @typescript-eslint/class-literal-property-style: ["error", "fields"] */ | ||
|
||
class Mx { | ||
public readonly myField1 = 1; | ||
|
||
// not a literal | ||
public readonly myField2 = [1, 2, 3]; | ||
|
||
private readonly ['myField3'] = 'hello world'; | ||
|
||
public get myField4() { | ||
return `hello from ${window.location.href}`; | ||
} | ||
} | ||
``` | ||
|
||
Examples of **incorrect** code with the `fields` style: | ||
|
||
```ts | ||
/* eslint @typescript-eslint/class-literal-property-style: ["error", "fields"] */ | ||
|
||
class Mx { | ||
public static get myField1() { | ||
return 1; | ||
} | ||
|
||
private get ['myField2']() { | ||
return 'hello world'; | ||
} | ||
} | ||
``` | ||
|
||
#### The `getters` style | ||
|
||
This style checks for any `readonly` fields that are assigned literal values, and requires them to be defined as getters instead. | ||
This style pairs well with the [`@typescript-eslint/prefer-readonly`](prefer-readonly.md) rule, | ||
as it will identify fields that can be `readonly`, and thus should be made into getters. | ||
|
||
Examples of **correct** code with the `getters` style: | ||
|
||
```ts | ||
/* eslint @typescript-eslint/class-literal-property-style: ["error", "getters"] */ | ||
|
||
class Mx { | ||
// no readonly modifier | ||
public myField1 = 'hello'; | ||
|
||
// not a literal | ||
public readonly myField2 = [1, 2, 3]; | ||
|
||
public static get myField3() { | ||
return 1; | ||
} | ||
|
||
private get ['myField4']() { | ||
return 'hello world'; | ||
} | ||
} | ||
``` | ||
|
||
Examples of **incorrect** code with the `getters` style: | ||
|
||
```ts | ||
/* eslint @typescript-eslint/class-literal-property-style: ["error", "getters"] */ | ||
|
||
class Mx { | ||
readonly myField1 = 1; | ||
readonly myField2 = `hello world`; | ||
private readonly myField3 = 'hello world'; | ||
} | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
When you have no strong preference, or do not wish to enforce a particular style | ||
for how literal values are exposed by your classes. |
This file contains 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
136 changes: 136 additions & 0 deletions
136
packages/eslint-plugin/src/rules/class-literal-property-style.ts
This file contains 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,136 @@ | ||
import { | ||
AST_NODE_TYPES, | ||
TSESTree, | ||
} from '@typescript-eslint/experimental-utils'; | ||
import * as util from '../util'; | ||
|
||
type Options = ['fields' | 'getters']; | ||
type MessageIds = 'preferFieldStyle' | 'preferGetterStyle'; | ||
|
||
interface NodeWithModifiers { | ||
accessibility?: TSESTree.Accessibility; | ||
static: boolean; | ||
} | ||
|
||
const printNodeModifiers = ( | ||
node: NodeWithModifiers, | ||
final: 'readonly' | 'get', | ||
): string => | ||
`${node.accessibility ?? ''}${ | ||
node.static ? ' static' : '' | ||
} ${final} `.trimLeft(); | ||
|
||
const isSupportedLiteral = ( | ||
node: TSESTree.Node, | ||
): node is TSESTree.LiteralExpression => { | ||
if ( | ||
node.type === AST_NODE_TYPES.Literal || | ||
node.type === AST_NODE_TYPES.BigIntLiteral | ||
) { | ||
return true; | ||
} | ||
|
||
if ( | ||
node.type === AST_NODE_TYPES.TaggedTemplateExpression || | ||
node.type === AST_NODE_TYPES.TemplateLiteral | ||
) { | ||
return ('quasi' in node ? node.quasi.quasis : node.quasis).length === 1; | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
export default util.createRule<Options, MessageIds>({ | ||
name: 'class-literal-property-style', | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'Ensures that literals on classes are exposed in a consistent style', | ||
category: 'Best Practices', | ||
recommended: false, | ||
}, | ||
fixable: 'code', | ||
messages: { | ||
preferFieldStyle: 'Literals should be exposed using readonly fields.', | ||
preferGetterStyle: 'Literals should be exposed using getters.', | ||
}, | ||
schema: [{ enum: ['fields', 'getters'] }], | ||
}, | ||
defaultOptions: ['fields'], | ||
create(context, [style]) { | ||
if (style === 'fields') { | ||
return { | ||
MethodDefinition(node: TSESTree.MethodDefinition): void { | ||
if ( | ||
node.kind !== 'get' || | ||
!node.value.body || | ||
!node.value.body.body.length | ||
) { | ||
return; | ||
} | ||
|
||
const [statement] = node.value.body.body; | ||
|
||
if (statement.type !== AST_NODE_TYPES.ReturnStatement) { | ||
return; | ||
} | ||
|
||
const { argument } = statement; | ||
|
||
if (!argument || !isSupportedLiteral(argument)) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node: node.key, | ||
messageId: 'preferFieldStyle', | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const name = sourceCode.getText(node.key); | ||
|
||
let text = ''; | ||
|
||
text += printNodeModifiers(node, 'readonly'); | ||
text += node.computed ? `[${name}]` : name; | ||
text += ` = ${sourceCode.getText(argument)};`; | ||
|
||
return fixer.replaceText(node, text); | ||
}, | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
return { | ||
ClassProperty(node: TSESTree.ClassProperty): void { | ||
if (!node.readonly || node.declare) { | ||
return; | ||
} | ||
|
||
const { value } = node; | ||
|
||
if (!value || !isSupportedLiteral(value)) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node: node.key, | ||
messageId: 'preferGetterStyle', | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const name = sourceCode.getText(node.key); | ||
|
||
let text = ''; | ||
|
||
text += printNodeModifiers(node, 'get'); | ||
text += node.computed ? `[${name}]` : name; | ||
text += `() { return ${sourceCode.getText(value)}; }`; | ||
|
||
return fixer.replaceText(node, text); | ||
}, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains 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.