Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(eslint-plugin): [member-ordering] add a required option for required vs. optional member ordering#5965

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
JoshuaKGoldberg merged 15 commits intotypescript-eslint:mainfromasdf93074:feat/5596
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
00d0066
fix(eslint-plugin): [member-ordering] add requiredFirst as an option …
asdf93074Nov 10, 2022
c00c2a9
fix(eslint-plugin): [member-ordering] adding types so build passes.
asdf93074Nov 10, 2022
0920426
fix(eslint-plugin): [member-ordering] fixing types so build passes.
asdf93074Nov 10, 2022
bf90b29
fix(eslint-plugin): [member-ordering] refactoring getIndexOfLastRequi…
asdf93074Nov 10, 2022
afcb6b9
fix(eslint-plugin): [member-ordering] additional test cases and handl…
asdf93074Nov 10, 2022
8c3e9d4
fix(eslint-plugin): [member-ordering] linting fix.
asdf93074Nov 10, 2022
31e9a99
fix(eslint-plugin): [member-ordering] change requiredFirst to require…
asdf93074Nov 15, 2022
ce40b8e
fix(eslint-plugin): [member-ordering] refactoring according to PR com…
asdf93074Nov 17, 2022
c09d42a
fix(eslint-plugin): [member-ordering] refactoring for PR and adding a…
asdf93074Nov 18, 2022
abf6b8d
fix(eslint-plugin): [member-ordering] refactoring for PR.
asdf93074Nov 18, 2022
f583399
fix(eslint-plugin): [member-ordering] adding test cases for coverage …
asdf93074Nov 18, 2022
1691fa7
fix(eslint-plugin): [member-ordering] increasing coverage to pass check.
asdf93074Nov 18, 2022
bb8444f
feat(eslint-plugin): [member-ordering] adding more tests to increase …
asdf93074Nov 21, 2022
832aa08
Updated name to optionalityOrder
JoshuaKGoldbergNov 28, 2022
17c0595
Merge branch 'main' into feat/5596
JoshuaKGoldbergNov 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletionpackages/eslint-plugin/docs/rules/member-ordering.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ type OrderConfig = MemberType[] | SortedOrderConfig | 'never';

interface SortedOrderConfig {
memberTypes?: MemberType[] | 'never';
optionalityOrder?: 'optional-first' | 'required-first';
order:
| 'alphabetically'
| 'alphabetically-case-insensitive'
Expand All@@ -44,9 +45,10 @@ You can configure `OrderConfig` options for:
- **`interfaces`**?: override ordering specifically for interfaces
- **`typeLiterals`**?: override ordering specifically for type literals

The `OrderConfig` settings for each kind of construct may configure sorting onone or both two levels:
The `OrderConfig` settings for each kind of construct may configure sorting onup to three levels:

- **`memberTypes`**: organizing on member type groups such as methods vs. properties
- **`optionalityOrder`**: whether to put all optional members first or all required members first
- **`order`**: organizing based on member names, such as alphabetically

### Groups
Expand DownExpand Up@@ -911,6 +913,96 @@ interface Foo {
}
```

#### Sorting Optional Members First or Last

The `optionalityOrder` option may be enabled to place all optional members in a group at the beginning or end of that group.

This config places all optional members before all required members:

```jsonc
// .eslintrc.json
{
"rules": {
"@typescript-eslint/member-ordering": [
"error",
{
"default": {
"optionalityOrder": "optional-first",
"order": "alphabetically"
}
}
]
}
}
```

<!--tabs-->

##### ❌ Incorrect

```ts
interface Foo {
a: boolean;
b?: number;
c: string;
}
```

##### ✅ Correct

```ts
interface Foo {
b?: number;
a: boolean;
c: string;
}
```

<!--/tabs-->

This config places all required members before all optional members:

```jsonc
// .eslintrc.json
{
"rules": {
"@typescript-eslint/member-ordering": [
"error",
{
"default": {
"optionalityOrder": "required-first",
"order": "alphabetically"
}
}
]
}
}
```

<!--tabs-->

##### ❌ Incorrect

```ts
interface Foo {
a: boolean;
b?: number;
c: string;
}
```

##### ✅ Correct

```ts
interface Foo {
a: boolean;
c: string;
b?: number;
}
```

<!--/tabs-->

## All Supported Options

### Member Types (Granular Form)
Expand Down
145 changes: 129 additions & 16 deletionspackages/eslint-plugin/src/rules/member-ordering.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,10 @@ import naturalCompare from 'natural-compare-lite';

import * as util from '../util';

export type MessageIds = 'incorrectGroupOrder' | 'incorrectOrder';
export type MessageIds =
| 'incorrectGroupOrder'
| 'incorrectOrder'
| 'incorrectRequiredMembersOrder';

type MemberKind =
| 'call-signature'
Expand DownExpand Up@@ -47,12 +50,15 @@ type Order = AlphabeticalOrder | 'as-written';

interface SortedOrderConfig {
memberTypes?: MemberType[] | 'never';
optionalityOrder?: OptionalityOrder;
order: Order;
}

type OrderConfig = MemberType[] | SortedOrderConfig | 'never';
type Member = TSESTree.ClassElement | TSESTree.TypeElement;

type OptionalityOrder = 'optional-first' | 'required-first';

export type Options = [
{
default?: OrderConfig;
Expand DownExpand Up@@ -101,6 +107,10 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({
'natural-case-insensitive',
],
},
optionalityOrder: {
type: 'string',
enum: ['optional-first', 'required-first'],
},
},
additionalProperties: false,
});
Expand DownExpand Up@@ -405,6 +415,26 @@ function getMemberName(
}
}

/**
* Returns true if the member is optional based on the member type.
*
* @param node the node to be evaluated.
*
* @returns Whether the member is optional, or false if it cannot be optional at all.
*/
function isMemberOptional(node: Member): boolean {
switch (node.type) {
case AST_NODE_TYPES.TSPropertySignature:
case AST_NODE_TYPES.TSMethodSignature:
case AST_NODE_TYPES.TSAbstractPropertyDefinition:
case AST_NODE_TYPES.PropertyDefinition:
case AST_NODE_TYPES.TSAbstractMethodDefinition:
case AST_NODE_TYPES.MethodDefinition:
return !!node.optional;
}
return false;
}

/**
* Gets the calculated rank using the provided method definition.
* The algorithm is as follows:
Expand DownExpand Up@@ -561,6 +591,7 @@ export default util.createRule<Options, MessageIds>({
'Member {{member}} should be declared before member {{beforeMember}}.',
incorrectGroupOrder:
'Member {{name}} should be declared before all {{rank}} definitions.',
incorrectRequiredMembersOrder: `Member {{member}} should be declared after all {{optionalOrRequired}} members.`,
},
schema: [
{
Expand DownExpand Up@@ -725,6 +756,59 @@ export default util.createRule<Options, MessageIds>({
}
}

/**
* Checks if the order of optional and required members is correct based
* on the given 'required' parameter.
*
* @param members Members to be validated.
* @param optionalityOrder Where to place optional members, if not intermixed.
*
* @return True if all required and optional members are correctly sorted.
*/
function checkRequiredOrder(
members: Member[],
optionalityOrder: OptionalityOrder | undefined,
): boolean {
const switchIndex = members.findIndex(
(member, i) =>
i && isMemberOptional(member) !== isMemberOptional(members[i - 1]),
);

const report = (member: Member): void =>
context.report({
messageId: 'incorrectRequiredMembersOrder',
loc: member.loc,
data: {
member: getMemberName(member, context.getSourceCode()),
optionalOrRequired:
optionalityOrder === 'optional-first' ? 'required' : 'optional',
},
});

// if the optionality of the first item is correct (based on optionalityOrder)
// then the first 0 inclusive to switchIndex exclusive members all
// have the correct optionality
if (
isMemberOptional(members[0]) !==
(optionalityOrder === 'required-first')
) {
report(members[0]);
return false;
}

for (let i = switchIndex + 1; i < members.length; i++) {
if (
isMemberOptional(members[i]) !==
isMemberOptional(members[switchIndex])
) {
report(members[switchIndex]);
return false;
}
}

return true;
}

/**
* Validates if all members are correctly sorted.
*
Expand All@@ -743,33 +827,62 @@ export default util.createRule<Options, MessageIds>({

// Standardize config
let order: Order | undefined;
let memberTypes;
let memberTypes: string | MemberType[] | undefined;
let optionalityOrder: OptionalityOrder | undefined;

// returns true if everything is good and false if an error was reported
const checkOrder = (memberSet: Member[]): boolean => {
const hasAlphaSort = !!(order && order !== 'as-written');

// Check order
if (Array.isArray(memberTypes)) {
const grouped = checkGroupSort(
memberSet,
memberTypes,
supportsModifiers,
);

if (grouped === null) {
return false;
}

if (hasAlphaSort) {
return !grouped.some(
groupMember =>
!checkAlphaSort(groupMember, order as AlphabeticalOrder),
);
}
} else if (hasAlphaSort) {
return checkAlphaSort(memberSet, order as AlphabeticalOrder);
}

return true;
};

if (Array.isArray(orderConfig)) {
memberTypes = orderConfig;
} else {
order = orderConfig.order;
memberTypes = orderConfig.memberTypes;
optionalityOrder = orderConfig.optionalityOrder;
}

const hasAlphaSort = !!(order && order !== 'as-written');
if (!optionalityOrder) {
checkOrder(members);
return;
}

// Check order
if (Array.isArray(memberTypes)) {
const grouped = checkGroupSort(members, memberTypes, supportsModifiers);
const switchIndex = members.findIndex(
(member, i) =>
i && isMemberOptional(member) !== isMemberOptional(members[i - 1]),
);

if (grouped === null) {
if (switchIndex !== -1) {
if (!checkRequiredOrder(members, optionalityOrder)) {
return;
}

if (hasAlphaSort) {
grouped.some(
groupMember =>
!checkAlphaSort(groupMember, order as AlphabeticalOrder),
);
}
} else if (hasAlphaSort) {
checkAlphaSort(members, order as AlphabeticalOrder);
checkOrder(members.slice(0, switchIndex));
checkOrder(members.slice(switchIndex));
}
}

Expand Down
24 changes: 24 additions & 0 deletionspackages/eslint-plugin/src/util/misc.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -180,6 +180,29 @@ function formatWordList(words: string[]): string {
return [words.slice(0, -1).join(', '), words.slice(-1)[0]].join(' and ');
}

/**
* Iterates the array in reverse and returns the index of the first element it
* finds which passes the predicate function.
*
* @returns Returns the index of the element if it finds it or -1 otherwise.
*/
function findLastIndex<T>(
members: T[],
predicate: (member: T) => boolean | undefined | null,
): number {
let idx = members.length - 1;

while (idx >= 0) {
const valid = predicate(members[idx]);
if (valid) {
return idx;
}
idx--;
}

return -1;
}

export {
arrayGroupByToMap,
arraysAreEqual,
Expand All@@ -194,4 +217,5 @@ export {
MemberNameType,
RequireKeys,
upperCaseFirst,
findLastIndex,
};
Loading

[8]ページ先頭

©2009-2025 Movatter.jp