Skip to content

Commit

Permalink
feat(eslint-plugin): [member-ordering] add support for grouping reado…
Browse files Browse the repository at this point in the history
…nly fields (#6349)

* feat(eslint-plugin): [member-ordering] add support for grouping readonly fields

* refactor: inline readonly assertions

* chore: remove comments

* test: add more tests for readonly TSAbstractPropertyDefinition and TSPropertySignature

* feat: add support for readonly signatures

---------

Co-authored-by: Josh Goldberg <git@joshuakgoldberg.com>
  • Loading branch information
IronGeek and JoshuaKGoldberg committed Mar 13, 2023
1 parent d55211c commit 9d3bdfc
Show file tree
Hide file tree
Showing 3 changed files with 729 additions and 14 deletions.
41 changes: 40 additions & 1 deletion packages/eslint-plugin/docs/rules/member-ordering.md
Expand Up @@ -58,7 +58,7 @@ The supported member attributes are, in order:

- **Accessibility** (`'public' | 'protected' | 'private' | '#private'`)
- **Decoration** (`'decorated'`): Whether the member has an explicit accessibility decorator
- **Kind** (`'call-signature' | 'constructor' | 'field' | 'get' | 'method' | 'set' | 'signature'`)
- **Kind** (`'call-signature' | 'constructor' | 'field' | 'readonly-field' | 'get' | 'method' | 'set' | 'signature' | 'readonly-signature'`)

Member attributes may be joined with a `'-'` to combine into more specific groups.
For example, `'public-field'` would come before `'private-field'`.
Expand Down Expand Up @@ -1014,37 +1014,60 @@ The most explicit and granular form is the following:
[
// Index signature
"signature",
"readonly-signature",

// Fields
"public-static-field",
"public-static-readonly-field",
"protected-static-field",
"protected-static-readonly-field",
"private-static-field",
"private-static-readonly-field",
"#private-static-field",
"#private-static-readonly-field",

"public-decorated-field",
"public-decorated-readonly-field",
"protected-decorated-field",
"protected-decorated-readonly-field",
"private-decorated-field",
"private-decorated-readonly-field",

"public-instance-field",
"public-instance-readonly-field",
"protected-instance-field",
"protected-instance-readonly-field",
"private-instance-field",
"private-instance-readonly-field",
"#private-instance-field",
"#private-instance-readonly-field",

"public-abstract-field",
"public-abstract-readonly-field",
"protected-abstract-field",
"protected-abstract-readonly-field",

"public-field",
"public-readonly-field",
"protected-field",
"protected-readonly-field",
"private-field",
"private-readonly-field"
"#private-field",
"#private-readonly-field"

"static-field",
"static-readonly-field",
"instance-field",
"instance-readonly-field"
"abstract-field",
"abstract-readonly-field",

"decorated-field",
"decorated-readonly-field",

"field",
"readonly-field",

// Static initialization
"static-initialization",
Expand Down Expand Up @@ -1290,6 +1313,22 @@ The third grouping option is to ignore both scope and accessibility.
]
```

### Member Group Types (Readonly Fields)

It is possible to group fields by their `readonly` modifiers.

```jsonc
[
// Index signature
"readonly-signature",
"signature",

// Fields
"readonly-field", // = ["public-static-readonly-field", "protected-static-readonly-field", "private-static-readonly-field", "public-instance-readonly-field", "protected-instance-readonly-field", "private-instance-readonly-field", "public-abstract-readonly-field", "protected-abstract-readonly-field"]
"field" // = ["public-static-field", "protected-static-field", "private-static-field", "public-instance-field", "protected-instance-field", "private-instance-field", "public-abstract-field", "protected-abstract-field"]
]
```

### Grouping Different Member Types at the Same Rank

It is also possible to group different member types at the same rank.
Expand Down
95 changes: 82 additions & 13 deletions packages/eslint-plugin/src/rules/member-ordering.ts
Expand Up @@ -9,19 +9,30 @@ export type MessageIds =
| 'incorrectOrder'
| 'incorrectRequiredMembersOrder';

type ReadonlyType = 'readonly-field' | 'readonly-signature';

type MemberKind =
| 'call-signature'
| 'constructor'
| ReadonlyType
| 'field'
| 'get'
| 'method'
| 'set'
| 'signature'
| 'static-initialization';

type DecoratedMemberKind = 'field' | 'method' | 'get' | 'set';
type DecoratedMemberKind =
| Exclude<ReadonlyType, 'readonly-signature'>
| 'field'
| 'method'
| 'get'
| 'set';

type NonCallableMemberKind = Exclude<MemberKind, 'constructor' | 'signature'>;
type NonCallableMemberKind = Exclude<
MemberKind,
'constructor' | 'signature' | 'readonly-signature'
>;

type MemberScope = 'static' | 'instance' | 'abstract';

Expand All @@ -31,7 +42,7 @@ type BaseMemberType =
| MemberKind
| `${Accessibility}-${Exclude<
MemberKind,
'signature' | 'static-initialization'
'signature' | 'readonly-signature' | 'static-initialization'
>}`
| `${Accessibility}-decorated-${DecoratedMemberKind}`
| `decorated-${DecoratedMemberKind}`
Expand Down Expand Up @@ -258,7 +269,9 @@ export const defaultOrder: MemberType[] = [
const allMemberTypes = Array.from(
(
[
'readonly-signature',
'signature',
'readonly-field',
'field',
'method',
'call-signature',
Expand All @@ -273,6 +286,7 @@ const allMemberTypes = Array.from(
(['public', 'protected', 'private', '#private'] as const).forEach(
accessibility => {
if (
type !== 'readonly-signature' &&
type !== 'signature' &&
type !== 'static-initialization' &&
type !== 'call-signature' &&
Expand All @@ -284,7 +298,8 @@ const allMemberTypes = Array.from(
// Only class instance fields, methods, get and set can have decorators attached to them
if (
accessibility !== '#private' &&
(type === 'field' ||
(type === 'readonly-field' ||
type === 'field' ||
type === 'method' ||
type === 'get' ||
type === 'set')
Expand All @@ -295,6 +310,7 @@ const allMemberTypes = Array.from(

if (
type !== 'constructor' &&
type !== 'readonly-signature' &&
type !== 'signature' &&
type !== 'call-signature'
) {
Expand Down Expand Up @@ -340,15 +356,17 @@ function getNodeType(node: Member): MemberKind | null {
case AST_NODE_TYPES.TSConstructSignatureDeclaration:
return 'constructor';
case AST_NODE_TYPES.TSAbstractPropertyDefinition:
return 'field';
return node.readonly ? 'readonly-field' : 'field';
case AST_NODE_TYPES.PropertyDefinition:
return node.value && functionExpressions.includes(node.value.type)
? 'method'
: node.readonly
? 'readonly-field'
: 'field';
case AST_NODE_TYPES.TSPropertySignature:
return 'field';
return node.readonly ? 'readonly-field' : 'field';
case AST_NODE_TYPES.TSIndexSignature:
return 'signature';
return node.readonly ? 'readonly-signature' : 'signature';
case AST_NODE_TYPES.StaticBlock:
return 'static-initialization';
default:
Expand Down Expand Up @@ -514,27 +532,50 @@ function getRank(
const decorated = 'decorators' in node && node.decorators!.length > 0;
if (
decorated &&
(type === 'field' ||
(type === 'readonly-field' ||
type === 'field' ||
type === 'method' ||
type === 'get' ||
type === 'set')
) {
memberGroups.push(`${accessibility}-decorated-${type}`);
memberGroups.push(`decorated-${type}`);

if (type === 'readonly-field') {
memberGroups.push(`${accessibility}-decorated-field`);
memberGroups.push(`decorated-field`);
}
}

if (type !== 'signature' && type !== 'static-initialization') {
if (
type !== 'readonly-signature' &&
type !== 'signature' &&
type !== 'static-initialization'
) {
if (type !== 'constructor') {
// Constructors have no scope
memberGroups.push(`${accessibility}-${scope}-${type}`);
memberGroups.push(`${scope}-${type}`);

if (type === 'readonly-field') {
memberGroups.push(`${accessibility}-${scope}-field`);
memberGroups.push(`${scope}-field`);
}
}

memberGroups.push(`${accessibility}-${type}`);
if (type === 'readonly-field') {
memberGroups.push(`${accessibility}-field`);
}
}
}

memberGroups.push(type);
if (type === 'readonly-signature') {
memberGroups.push('signature');
} else if (type === 'readonly-field') {
memberGroups.push('field');
}

// ...then get the rank order for those member groups based on the node
return getRankOrder(memberGroups, orderConfig);
Expand Down Expand Up @@ -621,15 +662,43 @@ export default util.createRule<Options, MessageIds>({
interfaces: {
oneOf: [
neverConfig,
arrayConfig(['signature', 'field', 'method', 'constructor']),
objectConfig(['signature', 'field', 'method', 'constructor']),
arrayConfig([
'readonly-signature',
'signature',
'readonly-field',
'field',
'method',
'constructor',
]),
objectConfig([
'readonly-signature',
'signature',
'readonly-field',
'field',
'method',
'constructor',
]),
],
},
typeLiterals: {
oneOf: [
neverConfig,
arrayConfig(['signature', 'field', 'method', 'constructor']),
objectConfig(['signature', 'field', 'method', 'constructor']),
arrayConfig([
'readonly-signature',
'signature',
'readonly-field',
'field',
'method',
'constructor',
]),
objectConfig([
'readonly-signature',
'signature',
'readonly-field',
'field',
'method',
'constructor',
]),
],
},
},
Expand Down

0 comments on commit 9d3bdfc

Please sign in to comment.