From 00d0066d5b7e80ea28cfe4d4e0b7455d46876f09 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Thu, 10 Nov 2022 17:18:34 +0500 Subject: [PATCH 01/14] fix(eslint-plugin): [member-ordering] add requiredFirst as an option which ensures that all required members appear before all optional members. --- .../src/rules/member-ordering.ts | 152 ++++++++++++++++-- .../member-ordering-required-first.test.ts | 84 ++++++++++ 2 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 3892c989bc9a..2984614d3c4a 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -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' + | 'incorrectRequiredFirstOrder'; type MemberKind = | 'call-signature' @@ -46,6 +49,7 @@ type Order = AlphabeticalOrder | 'as-written'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; order: Order; + requiredFirst?: boolean; } type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; @@ -99,6 +103,9 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({ 'natural-case-insensitive', ], }, + requiredFirst: { + type: 'boolean', + }, }, additionalProperties: false, }); @@ -376,6 +383,49 @@ function getMemberName( } } +/** + * Returns true if the member is optional based on the member type. + * + * @param node the node to be evaluated. + * @param sourceCode + */ +function isMemberOptional(node: Member): boolean | undefined { + 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; + case AST_NODE_TYPES.TSConstructSignatureDeclaration: + case AST_NODE_TYPES.TSCallSignatureDeclaration: + case AST_NODE_TYPES.TSIndexSignature: + case AST_NODE_TYPES.StaticBlock: + default: + return undefined; + } +} + +/** + * Gets the index of the + * + * @param node the node to be evaluated. + * @param sourceCode + */ +function getIndexOfLastRequiredMember(members: Member[]): number { + let idx = members + .slice() + .reverse() + .findIndex(member => !isMemberOptional(member)); + + if (idx != -1) { + idx = members.length - 1 - idx; + } + + return idx; +} + /** * Gets the calculated rank using the provided method definition. * The algorithm is as follows: @@ -525,6 +575,7 @@ export default util.createRule({ 'Member {{member}} should be declared before member {{beforeMember}}.', incorrectGroupOrder: 'Member {{name}} should be declared before all {{rank}} definitions.', + incorrectRequiredFirstOrder: `Required {{member}} should be declared before optional member {{beforeMember}}.`, }, schema: [ { @@ -689,6 +740,39 @@ export default util.createRule({ } } + /** + * Checks if all required members appear before all optional members. + * + * @param members Members to be validated. + * + * @return True if all required and optional members are correctly sorted. + */ + function checkRequiredFirstOrder(members: Member[]): boolean { + const lastRequiredMemberIndex = getIndexOfLastRequiredMember(members); + const firstOptionalMemberIndex = members.findIndex(member => + isMemberOptional(member), + ); + + if (firstOptionalMemberIndex < lastRequiredMemberIndex) { + context.report({ + messageId: 'incorrectRequiredFirstOrder', + loc: members[firstOptionalMemberIndex].loc, + data: { + member: getMemberName( + members[lastRequiredMemberIndex], + context.getSourceCode(), + ), + beforeMember: getMemberName( + members[firstOptionalMemberIndex], + context.getSourceCode(), + ), + }, + }); + } + + return firstOptionalMemberIndex > lastRequiredMemberIndex; + } + /** * Validates if all members are correctly sorted. * @@ -708,33 +792,69 @@ export default util.createRule({ // Standardize config let order: Order | undefined; let memberTypes; + let requiredFirst = false; + + const memberSets: Array = []; + + const checkOrder = (memberSet: Member[]): void => { + const hasAlphaSort = !!(order && order !== 'as-written'); + + // Check order + if (Array.isArray(memberTypes)) { + const grouped = checkGroupSort(members, memberTypes, supportsModifiers); + + if (grouped === null) { + return; + } + + if (hasAlphaSort) { + grouped.some( + groupMember => + !checkAlphaSort(groupMember, order as AlphabeticalOrder), + ); + } + } else if (hasAlphaSort) { + checkAlphaSort(members, order as AlphabeticalOrder); + } + }; if (Array.isArray(orderConfig)) { memberTypes = orderConfig; } else { order = orderConfig.order; memberTypes = orderConfig.memberTypes; + requiredFirst = orderConfig.requiredFirst; } - const hasAlphaSort = !!(order && order !== 'as-written'); - - // Check order - if (Array.isArray(memberTypes)) { - const grouped = checkGroupSort(members, memberTypes, supportsModifiers); - - if (grouped === null) { + if (requiredFirst) { + if (!checkRequiredFirstOrder(members)) { return; } - if (hasAlphaSort) { - grouped.some( - groupMember => - !checkAlphaSort(groupMember, order as AlphabeticalOrder), - ); - } - } else if (hasAlphaSort) { - checkAlphaSort(members, order as AlphabeticalOrder); + // if the order of required and optional elements is correct, + // then check for correct order within the required and + // optional member sets + const lastRequiredMemberIndex = getIndexOfLastRequiredMember(members); + const firstOptionalMemberIndex = members.findIndex(member => + isMemberOptional(member), + ); + + const requiredMembers: Member[] = members.slice( + 0, + lastRequiredMemberIndex + 1, + ); + const optionalMembers: Member[] = members.slice( + firstOptionalMemberIndex, + ); + + memberSets.push(requiredMembers, optionalMembers); } + + if (memberSets.length === 0) { + memberSets.push(members); + } + + memberSets.forEach(checkOrder); } return { diff --git a/packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts b/packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts new file mode 100644 index 000000000000..adc5beb1fcfb --- /dev/null +++ b/packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts @@ -0,0 +1,84 @@ +import type { TSESLint } from '@typescript-eslint/utils'; + +import type { MessageIds, Options } from '../../src/rules/member-ordering'; +import rule from '../../src/rules/member-ordering'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const grouped: TSESLint.RunTests = { + valid: [ + { + code: ` +interface X { + c: string; + b?: string; + d?: string; +} `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + requiredFirst: true, + }, + }, + ], + }, + ], + invalid: [ + { + code: ` +interface X { + m: string; + d?: string; + b?: string; +} `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + requiredFirst: true, + }, + }, + ], + errors: [ + { + messageId: 'incorrectOrder', + line: 5, + column: 3, + }, + ], + }, + { + code: ` +interface X { + a: string; + b?: string; + c: string; +} + `, + options: [ + { + default: { + memberTypes: ['call-signature', 'field', 'method'], + order: 'as-written', + requiredFirst: true, + }, + }, + ], + errors: [ + { + messageId: 'incorrectRequiredFirstOrder', + line: 4, + column: 3, + }, + ], + }, + ], +}; + +ruleTester.run('member-ordering-required-first', rule, grouped); From c00c2a9efcabcdea2227ad5dd509097a5d3c8bd8 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Thu, 10 Nov 2022 17:52:45 +0500 Subject: [PATCH 02/14] fix(eslint-plugin): [member-ordering] adding types so build passes. --- packages/eslint-plugin/src/rules/member-ordering.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 2984614d3c4a..92c7001fe0eb 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -791,7 +791,7 @@ export default util.createRule({ // Standardize config let order: Order | undefined; - let memberTypes; + let memberTypes: string | MemberType[] | undefined; let requiredFirst = false; const memberSets: Array = []; @@ -801,7 +801,11 @@ export default util.createRule({ // Check order if (Array.isArray(memberTypes)) { - const grouped = checkGroupSort(members, memberTypes, supportsModifiers); + const grouped = checkGroupSort( + memberSet, + memberTypes, + supportsModifiers, + ); if (grouped === null) { return; @@ -814,7 +818,7 @@ export default util.createRule({ ); } } else if (hasAlphaSort) { - checkAlphaSort(members, order as AlphabeticalOrder); + checkAlphaSort(memberSet, order as AlphabeticalOrder); } }; From 0920426861bc8b5e6e459f4627aceccd30ee7794 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Thu, 10 Nov 2022 17:59:27 +0500 Subject: [PATCH 03/14] fix(eslint-plugin): [member-ordering] fixing types so build passes. --- packages/eslint-plugin/src/rules/member-ordering.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 92c7001fe0eb..9c51334e1ae1 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -792,7 +792,7 @@ export default util.createRule({ // Standardize config let order: Order | undefined; let memberTypes: string | MemberType[] | undefined; - let requiredFirst = false; + let requiredFirst: boolean | undefined = false; const memberSets: Array = []; From bf90b292a0f9966a22ca4cec50abcc638fa3c850 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Thu, 10 Nov 2022 20:15:59 +0500 Subject: [PATCH 04/14] fix(eslint-plugin): [member-ordering] refactoring getIndexOfLastRequiredMember to be slightly faster and adding jsdoc comments for it and isMemberOptional. --- .../src/rules/member-ordering.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 9c51334e1ae1..1681282d4596 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -387,7 +387,8 @@ function getMemberName( * Returns true if the member is optional based on the member type. * * @param node the node to be evaluated. - * @param sourceCode + * + * @returns {Boolean} Returns true if the member is optional, false if it is not and undefined if it cannot be optional at all. */ function isMemberOptional(node: Member): boolean | undefined { switch (node.type) { @@ -408,22 +409,29 @@ function isMemberOptional(node: Member): boolean | undefined { } /** - * Gets the index of the + * Gets the index of the last required member in the array. * - * @param node the node to be evaluated. - * @param sourceCode + * @example + * // returns 5 + * getIndexOfLastRequiredMember([ req, req, req, optional, req, req, optional ]) + * // 0 1 2 3 4 5 6 + * + * @param {Member[]} members An array of Member nodes containing required and optional items. + * + * @returns {Number} Returns the index of the element if it finds it or -1 otherwise. */ function getIndexOfLastRequiredMember(members: Member[]): number { - let idx = members - .slice() - .reverse() - .findIndex(member => !isMemberOptional(member)); + let idx = members.length - 1; - if (idx != -1) { - idx = members.length - 1 - idx; + while (idx >= 0) { + const isMemberRequired = !isMemberOptional(members[idx]); + if (isMemberRequired) { + return idx; + } + idx--; } - return idx; + return -1; } /** From afcb6b97229e527adea871b1c7ba66b5e15e6e7e Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Thu, 10 Nov 2022 20:46:00 +0500 Subject: [PATCH 05/14] fix(eslint-plugin): [member-ordering] additional test cases and handling for them. --- .../src/rules/member-ordering.ts | 29 +++++++++----- .../member-ordering-required-first.test.ts | 40 +++++++++++++++++-- 2 files changed, 57 insertions(+), 12 deletions(-) rename packages/eslint-plugin/tests/rules/{ => member-ordering}/member-ordering-required-first.test.ts (66%) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 1681282d4596..35b586638de2 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -751,7 +751,7 @@ export default util.createRule({ /** * Checks if all required members appear before all optional members. * - * @param members Members to be validated. + * @param {Member[]} members Members to be validated. * * @return True if all required and optional members are correctly sorted. */ @@ -761,6 +761,12 @@ export default util.createRule({ isMemberOptional(member), ); + // if the array is either all required members or all optional members + // then its already in required first order + if (firstOptionalMemberIndex === -1 || lastRequiredMemberIndex === -1) { + return true; + } + if (firstOptionalMemberIndex < lastRequiredMemberIndex) { context.report({ messageId: 'incorrectRequiredFirstOrder', @@ -851,15 +857,20 @@ export default util.createRule({ isMemberOptional(member), ); - const requiredMembers: Member[] = members.slice( - 0, - lastRequiredMemberIndex + 1, - ); - const optionalMembers: Member[] = members.slice( - firstOptionalMemberIndex, - ); + if (lastRequiredMemberIndex != -1) { + const requiredMembers: Member[] = members.slice( + 0, + lastRequiredMemberIndex + 1, + ); + memberSets.push(requiredMembers); + } - memberSets.push(requiredMembers, optionalMembers); + if (firstOptionalMemberIndex != -1) { + const optionalMembers: Member[] = members.slice( + firstOptionalMemberIndex, + ); + memberSets.push(optionalMembers); + } } if (memberSets.length === 0) { diff --git a/packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts similarity index 66% rename from packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts rename to packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts index adc5beb1fcfb..4b8decbee960 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering-required-first.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts @@ -1,8 +1,8 @@ import type { TSESLint } from '@typescript-eslint/utils'; -import type { MessageIds, Options } from '../../src/rules/member-ordering'; -import rule from '../../src/rules/member-ordering'; -import { RuleTester } from '../RuleTester'; +import type { MessageIds, Options } from '../../../src/rules/member-ordering'; +import rule from '../../../src/rules/member-ordering'; +import { RuleTester } from '../../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -27,6 +27,40 @@ interface X { }, ], }, + { + code: ` +interface X { + b?: string; + c?: string; + d?: string; +} `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + requiredFirst: true, + }, + }, + ], + }, + { + code: ` +interface X { + b: string; + c: string; + d: string; +} `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + requiredFirst: true, + }, + }, + ], + }, ], invalid: [ { From 8c3e9d4315c9367fa9cdc86103bbcf954622b57e Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Thu, 10 Nov 2022 21:58:03 +0500 Subject: [PATCH 06/14] fix(eslint-plugin): [member-ordering] linting fix. --- .../member-ordering-required-first.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts index 4b8decbee960..5ba03fd22e80 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts @@ -16,7 +16,8 @@ interface X { c: string; b?: string; d?: string; -} `, +} + `, options: [ { default: { @@ -33,7 +34,8 @@ interface X { b?: string; c?: string; d?: string; -} `, +} + `, options: [ { default: { @@ -50,7 +52,8 @@ interface X { b: string; c: string; d: string; -} `, +} + `, options: [ { default: { @@ -69,7 +72,8 @@ interface X { m: string; d?: string; b?: string; -} `, +} + `, options: [ { default: { @@ -94,7 +98,7 @@ interface X { b?: string; c: string; } - `, + `, options: [ { default: { From 31e9a99988fc4b6123dded28ca35eeefbb78ba18 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Tue, 15 Nov 2022 16:06:02 +0500 Subject: [PATCH 07/14] fix(eslint-plugin): [member-ordering] change requiredFirst to required which takes first or last as a value and adding functionality to check order based on both of these along with additional tests. --- .../src/rules/member-ordering.ts | 139 ++++++---- .../member-ordering-required-first.test.ts | 122 --------- .../member-ordering-required.test.ts | 238 ++++++++++++++++++ 3 files changed, 331 insertions(+), 168 deletions(-) delete mode 100644 packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts create mode 100644 packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 35b586638de2..af169097fcb7 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -7,7 +7,7 @@ import * as util from '../util'; export type MessageIds = | 'incorrectGroupOrder' | 'incorrectOrder' - | 'incorrectRequiredFirstOrder'; + | 'incorrectRequiredMembersOrder'; type MemberKind = | 'call-signature' @@ -49,7 +49,7 @@ type Order = AlphabeticalOrder | 'as-written'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; order: Order; - requiredFirst?: boolean; + required?: 'first' | 'last'; } type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; @@ -103,8 +103,9 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({ 'natural-case-insensitive', ], }, - requiredFirst: { - type: 'boolean', + required: { + type: 'string', + enum: ['first', 'last'], }, }, additionalProperties: false, @@ -409,23 +410,29 @@ function isMemberOptional(node: Member): boolean | undefined { } /** - * Gets the index of the last required member in the array. + * Iterates the array in reverse and returns the index of the first element it + * finds which passes the predicate function. * * @example + * ```js + * const isMemberRequired = (member) => !isMemberOptional(member); * // returns 5 - * getIndexOfLastRequiredMember([ req, req, req, optional, req, req, optional ]) - * // 0 1 2 3 4 5 6 - * + * findLastIndexOfMember([ req, req, req, optional, req, req, optional ], isMemberRequired) + * // 0 1 2 3 4 5 6 + * ``` * @param {Member[]} members An array of Member nodes containing required and optional items. * * @returns {Number} Returns the index of the element if it finds it or -1 otherwise. */ -function getIndexOfLastRequiredMember(members: Member[]): number { +function findLastIndex( + members: T[], + predicate: (member: T) => boolean | undefined | null, +): number { let idx = members.length - 1; while (idx >= 0) { - const isMemberRequired = !isMemberOptional(members[idx]); - if (isMemberRequired) { + const valid = predicate(members[idx]); + if (valid) { return idx; } idx--; @@ -583,7 +590,7 @@ export default util.createRule({ 'Member {{member}} should be declared before member {{beforeMember}}.', incorrectGroupOrder: 'Member {{name}} should be declared before all {{rank}} definitions.', - incorrectRequiredFirstOrder: `Required {{member}} should be declared before optional member {{beforeMember}}.`, + incorrectRequiredMembersOrder: `Member {{member}} should be declared after all {{optionalOrRequired}} members.`, }, schema: [ { @@ -749,42 +756,50 @@ export default util.createRule({ } /** - * Checks if all required members appear before all optional members. + * Checks if the order of optional and required members is correct based + * on the given 'required' parameter. * * @param {Member[]} members Members to be validated. * * @return True if all required and optional members are correctly sorted. */ - function checkRequiredFirstOrder(members: Member[]): boolean { - const lastRequiredMemberIndex = getIndexOfLastRequiredMember(members); - const firstOptionalMemberIndex = members.findIndex(member => - isMemberOptional(member), - ); + function checkRequiredOrder( + members: Member[], + required: 'first' | 'last' | undefined, + ): boolean { + if (!required) { + return true; + } + + let firstIdx = -1; + let lastIdx = -1; + + if (required === 'first') { + firstIdx = members.findIndex(member => isMemberOptional(member)); + lastIdx = findLastIndex(members, m => !isMemberOptional(m)); + } else if (required === 'last') { + firstIdx = members.findIndex(member => !isMemberOptional(member)); + lastIdx = findLastIndex(members, isMemberOptional); + } // if the array is either all required members or all optional members // then its already in required first order - if (firstOptionalMemberIndex === -1 || lastRequiredMemberIndex === -1) { + if (firstIdx === -1 || lastIdx === -1) { return true; } - if (firstOptionalMemberIndex < lastRequiredMemberIndex) { + if (firstIdx < lastIdx) { context.report({ - messageId: 'incorrectRequiredFirstOrder', - loc: members[firstOptionalMemberIndex].loc, + messageId: 'incorrectRequiredMembersOrder', + loc: members[firstIdx].loc, data: { - member: getMemberName( - members[lastRequiredMemberIndex], - context.getSourceCode(), - ), - beforeMember: getMemberName( - members[firstOptionalMemberIndex], - context.getSourceCode(), - ), + member: getMemberName(members[firstIdx], context.getSourceCode()), + optionalOrRequired: required === 'first' ? 'required' : 'optional', }, }); } - return firstOptionalMemberIndex > lastRequiredMemberIndex; + return firstIdx > lastIdx; } /** @@ -806,11 +821,12 @@ export default util.createRule({ // Standardize config let order: Order | undefined; let memberTypes: string | MemberType[] | undefined; - let requiredFirst: boolean | undefined = false; + let required: 'first' | 'last' | undefined; const memberSets: Array = []; - const checkOrder = (memberSet: Member[]): void => { + // 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 @@ -822,18 +838,20 @@ export default util.createRule({ ); if (grouped === null) { - return; + return false; } if (hasAlphaSort) { - grouped.some( + return !grouped.some( groupMember => !checkAlphaSort(groupMember, order as AlphabeticalOrder), ); } } else if (hasAlphaSort) { - checkAlphaSort(memberSet, order as AlphabeticalOrder); + return checkAlphaSort(memberSet, order as AlphabeticalOrder); } + + return true; }; if (Array.isArray(orderConfig)) { @@ -841,21 +859,31 @@ export default util.createRule({ } else { order = orderConfig.order; memberTypes = orderConfig.memberTypes; - requiredFirst = orderConfig.requiredFirst; + required = orderConfig.required; } - if (requiredFirst) { - if (!checkRequiredFirstOrder(members)) { - return; - } + if (!checkRequiredOrder(members, required)) { + return; + } + if (required === 'first') { // if the order of required and optional elements is correct, - // then check for correct order within the required and + // then check for correct sort and group order within the required and // optional member sets - const lastRequiredMemberIndex = getIndexOfLastRequiredMember(members); const firstOptionalMemberIndex = members.findIndex(member => isMemberOptional(member), ); + const lastRequiredMemberIndex = findLastIndex( + members, + m => !isMemberOptional(m), + ); + + if (firstOptionalMemberIndex != -1) { + const optionalMembers: Member[] = members.slice( + firstOptionalMemberIndex, + ); + memberSets.push(optionalMembers); + } if (lastRequiredMemberIndex != -1) { const requiredMembers: Member[] = members.slice( @@ -864,10 +892,29 @@ export default util.createRule({ ); memberSets.push(requiredMembers); } + } else if (required === 'last') { + // if the order of required and optional elements is correct, + // then check for correct order within the required and + // optional member sets + const firstRequiredMemberIndex = members.findIndex( + member => !isMemberOptional(member), + ); + const lastOptionalMemberIndex = findLastIndex( + members, + isMemberOptional, + ); - if (firstOptionalMemberIndex != -1) { + if (firstRequiredMemberIndex != -1) { + const requiredMembers: Member[] = members.slice( + firstRequiredMemberIndex, + ); + memberSets.push(requiredMembers); + } + + if (lastOptionalMemberIndex != -1) { const optionalMembers: Member[] = members.slice( - firstOptionalMemberIndex, + 0, + lastOptionalMemberIndex + 1, ); memberSets.push(optionalMembers); } @@ -877,7 +924,7 @@ export default util.createRule({ memberSets.push(members); } - memberSets.forEach(checkOrder); + memberSets.every(checkOrder); } return { diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts deleted file mode 100644 index 5ba03fd22e80..000000000000 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required-first.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { TSESLint } from '@typescript-eslint/utils'; - -import type { MessageIds, Options } from '../../../src/rules/member-ordering'; -import rule from '../../../src/rules/member-ordering'; -import { RuleTester } from '../../RuleTester'; - -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', -}); - -const grouped: TSESLint.RunTests = { - valid: [ - { - code: ` -interface X { - c: string; - b?: string; - d?: string; -} - `, - options: [ - { - default: { - memberTypes: 'never', - order: 'alphabetically', - requiredFirst: true, - }, - }, - ], - }, - { - code: ` -interface X { - b?: string; - c?: string; - d?: string; -} - `, - options: [ - { - default: { - memberTypes: 'never', - order: 'as-written', - requiredFirst: true, - }, - }, - ], - }, - { - code: ` -interface X { - b: string; - c: string; - d: string; -} - `, - options: [ - { - default: { - memberTypes: 'never', - order: 'as-written', - requiredFirst: true, - }, - }, - ], - }, - ], - invalid: [ - { - code: ` -interface X { - m: string; - d?: string; - b?: string; -} - `, - options: [ - { - default: { - memberTypes: 'never', - order: 'alphabetically', - requiredFirst: true, - }, - }, - ], - errors: [ - { - messageId: 'incorrectOrder', - line: 5, - column: 3, - }, - ], - }, - { - code: ` -interface X { - a: string; - b?: string; - c: string; -} - `, - options: [ - { - default: { - memberTypes: ['call-signature', 'field', 'method'], - order: 'as-written', - requiredFirst: true, - }, - }, - ], - errors: [ - { - messageId: 'incorrectRequiredFirstOrder', - line: 4, - column: 3, - }, - ], - }, - ], -}; - -ruleTester.run('member-ordering-required-first', rule, grouped); diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts new file mode 100644 index 000000000000..bf5389157f55 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts @@ -0,0 +1,238 @@ +import type { TSESLint } from '@typescript-eslint/utils'; + +import type { MessageIds, Options } from '../../../src/rules/member-ordering'; +import rule from '../../../src/rules/member-ordering'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const grouped: TSESLint.RunTests = { + valid: [ + // required - first + { + code: ` +interface X { + c: string; + b?: string; + d?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, + { + code: ` +interface X { + b?: string; + c?: string; + d?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'first', + }, + }, + ], + }, + { + code: ` +interface X { + b: string; + c: string; + d: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'first', + }, + }, + ], + }, + // required - last + { + code: ` +interface X { + b?: string; + d?: string; + c: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'last', + }, + }, + ], + }, + { + code: ` +interface X { + b?: string; + c?: string; + d?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'last', + }, + }, + ], + }, + { + code: ` +interface X { + b: string; + c: string; + d: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'last', + }, + }, + ], + }, + ], + // required - first + invalid: [ + { + code: ` +interface X { + m: string; + d?: string; + b?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + errors: [ + { + messageId: 'incorrectOrder', + line: 5, + column: 3, + }, + ], + }, + { + code: ` +interface X { + a: string; + b?: string; + c: string; +} + `, + options: [ + { + default: { + memberTypes: ['call-signature', 'field', 'method'], + order: 'as-written', + required: 'first', + }, + }, + ], + errors: [ + { + messageId: 'incorrectRequiredMembersOrder', + line: 4, + column: 3, + data: { + member: 'b', + optionalOrRequired: 'required', + }, + }, + ], + }, + // required - last + { + code: ` +interface X { + d?: string; + b?: string; + m: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'last', + }, + }, + ], + errors: [ + { + messageId: 'incorrectOrder', + line: 4, + column: 3, + }, + ], + }, + { + code: ` +interface X { + a?: string; + b: string; + c?: string; +} + `, + options: [ + { + default: { + memberTypes: ['call-signature', 'field', 'method'], + order: 'as-written', + required: 'last', + }, + }, + ], + errors: [ + { + messageId: 'incorrectRequiredMembersOrder', + line: 4, + column: 3, + data: { + member: 'b', + optionalOrRequired: 'optional', + }, + }, + ], + }, + ], +}; + +ruleTester.run('member-ordering-required', rule, grouped); From ce40b8ebe8c93ed0354e61538159eb666aa6d8a7 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Fri, 18 Nov 2022 00:58:01 +0500 Subject: [PATCH 08/14] fix(eslint-plugin): [member-ordering] refactoring according to PR comments. --- .../src/rules/member-ordering.ts | 113 +++++------------- packages/eslint-plugin/src/util/misc.ts | 24 ++++ 2 files changed, 54 insertions(+), 83 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index af169097fcb7..0a50f7db8bbf 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -389,7 +389,7 @@ function getMemberName( * * @param node the node to be evaluated. * - * @returns {Boolean} Returns true if the member is optional, false if it is not and undefined if it cannot be optional at all. + * @returns Whether the member is optional, or false if it cannot be optional at all. */ function isMemberOptional(node: Member): boolean | undefined { switch (node.type) { @@ -400,45 +400,8 @@ function isMemberOptional(node: Member): boolean | undefined { case AST_NODE_TYPES.TSAbstractMethodDefinition: case AST_NODE_TYPES.MethodDefinition: return node.optional; - case AST_NODE_TYPES.TSConstructSignatureDeclaration: - case AST_NODE_TYPES.TSCallSignatureDeclaration: - case AST_NODE_TYPES.TSIndexSignature: - case AST_NODE_TYPES.StaticBlock: - default: - return undefined; - } -} - -/** - * Iterates the array in reverse and returns the index of the first element it - * finds which passes the predicate function. - * - * @example - * ```js - * const isMemberRequired = (member) => !isMemberOptional(member); - * // returns 5 - * findLastIndexOfMember([ req, req, req, optional, req, req, optional ], isMemberRequired) - * // 0 1 2 3 4 5 6 - * ``` - * @param {Member[]} members An array of Member nodes containing required and optional items. - * - * @returns {Number} Returns the index of the element if it finds it or -1 otherwise. - */ -function findLastIndex( - 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; + return false; } /** @@ -759,7 +722,7 @@ export default util.createRule({ * Checks if the order of optional and required members is correct based * on the given 'required' parameter. * - * @param {Member[]} members Members to be validated. + * @param members Members to be validated. * * @return True if all required and optional members are correctly sorted. */ @@ -771,35 +734,33 @@ export default util.createRule({ return true; } - let firstIdx = -1; - let lastIdx = -1; - - if (required === 'first') { - firstIdx = members.findIndex(member => isMemberOptional(member)); - lastIdx = findLastIndex(members, m => !isMemberOptional(m)); - } else if (required === 'last') { - firstIdx = members.findIndex(member => !isMemberOptional(member)); - lastIdx = findLastIndex(members, isMemberOptional); - } + const [firstIdx, lastIdx] = + required === 'first' + ? [ + members.findIndex(isMemberOptional), + util.findLastIndex(members, m => !isMemberOptional(m)), + ] + : [ + members.findIndex(member => !isMemberOptional(member)), + util.findLastIndex(members, isMemberOptional), + ]; // if the array is either all required members or all optional members - // then its already in required first order - if (firstIdx === -1 || lastIdx === -1) { + // then it is already in the correct order + if (firstIdx === -1 || lastIdx === -1 || firstIdx > lastIdx) { return true; } - if (firstIdx < lastIdx) { - context.report({ - messageId: 'incorrectRequiredMembersOrder', - loc: members[firstIdx].loc, - data: { - member: getMemberName(members[firstIdx], context.getSourceCode()), - optionalOrRequired: required === 'first' ? 'required' : 'optional', - }, - }); - } + context.report({ + messageId: 'incorrectRequiredMembersOrder', + loc: members[firstIdx].loc, + data: { + member: getMemberName(members[firstIdx], context.getSourceCode()), + optionalOrRequired: required === 'first' ? 'required' : 'optional', + }, + }); - return firstIdx > lastIdx; + return false; } /** @@ -873,24 +834,17 @@ export default util.createRule({ const firstOptionalMemberIndex = members.findIndex(member => isMemberOptional(member), ); - const lastRequiredMemberIndex = findLastIndex( + const lastRequiredMemberIndex = util.findLastIndex( members, m => !isMemberOptional(m), ); if (firstOptionalMemberIndex != -1) { - const optionalMembers: Member[] = members.slice( - firstOptionalMemberIndex, - ); - memberSets.push(optionalMembers); + memberSets.push(members.slice(firstOptionalMemberIndex)); } if (lastRequiredMemberIndex != -1) { - const requiredMembers: Member[] = members.slice( - 0, - lastRequiredMemberIndex + 1, - ); - memberSets.push(requiredMembers); + memberSets.push(members.slice(0, lastRequiredMemberIndex + 1)); } } else if (required === 'last') { // if the order of required and optional elements is correct, @@ -899,24 +853,17 @@ export default util.createRule({ const firstRequiredMemberIndex = members.findIndex( member => !isMemberOptional(member), ); - const lastOptionalMemberIndex = findLastIndex( + const lastOptionalMemberIndex = util.findLastIndex( members, isMemberOptional, ); if (firstRequiredMemberIndex != -1) { - const requiredMembers: Member[] = members.slice( - firstRequiredMemberIndex, - ); - memberSets.push(requiredMembers); + memberSets.push(members.slice(firstRequiredMemberIndex)); } if (lastOptionalMemberIndex != -1) { - const optionalMembers: Member[] = members.slice( - 0, - lastOptionalMemberIndex + 1, - ); - memberSets.push(optionalMembers); + memberSets.push(members.slice(0, lastOptionalMemberIndex + 1)); } } diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 2bdc8ee0f591..351c94a6361f 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -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( + 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, @@ -194,4 +217,5 @@ export { MemberNameType, RequireKeys, upperCaseFirst, + findLastIndex, }; From c09d42a215c35b97ea94503f5d8ff0309e7f6c86 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Fri, 18 Nov 2022 18:29:38 +0500 Subject: [PATCH 09/14] fix(eslint-plugin): [member-ordering] refactoring for PR and adding another test case. --- .../src/rules/member-ordering.ts | 118 +++++++----------- .../member-ordering-required.test.ts | 33 +++++ 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 0a50f7db8bbf..914604b41ca7 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -391,7 +391,7 @@ function getMemberName( * * @returns Whether the member is optional, or false if it cannot be optional at all. */ -function isMemberOptional(node: Member): boolean | undefined { +function isMemberOptional(node: Member): boolean { switch (node.type) { case AST_NODE_TYPES.TSPropertySignature: case AST_NODE_TYPES.TSMethodSignature: @@ -399,7 +399,7 @@ function isMemberOptional(node: Member): boolean | undefined { case AST_NODE_TYPES.PropertyDefinition: case AST_NODE_TYPES.TSAbstractMethodDefinition: case AST_NODE_TYPES.MethodDefinition: - return node.optional; + return !!node.optional; } return false; } @@ -734,33 +734,45 @@ export default util.createRule({ return true; } - const [firstIdx, lastIdx] = - required === 'first' - ? [ - members.findIndex(isMemberOptional), - util.findLastIndex(members, m => !isMemberOptional(m)), - ] - : [ - members.findIndex(member => !isMemberOptional(member)), - util.findLastIndex(members, isMemberOptional), - ]; - - // if the array is either all required members or all optional members - // then it is already in the correct order - if (firstIdx === -1 || lastIdx === -1 || firstIdx > lastIdx) { + const switchIndex = members.findIndex( + (member, i) => + i && isMemberOptional(member) !== isMemberOptional(members[i - 1]), + ); + + if (switchIndex === -1) { return true; } - context.report({ - messageId: 'incorrectRequiredMembersOrder', - loc: members[firstIdx].loc, - data: { - member: getMemberName(members[firstIdx], context.getSourceCode()), - optionalOrRequired: required === 'first' ? 'required' : 'optional', - }, - }); + const report = (member: Member): void => + context.report({ + messageId: 'incorrectRequiredMembersOrder', + loc: member.loc, + data: { + member: getMemberName(member, context.getSourceCode()), + optionalOrRequired: required === 'first' ? 'required' : 'optional', + }, + }); + + // if the optionality of the first item is correct (based on required) + // then the first 0 inclusive to switchIndex exclusive members all + // have the correct optionality + if (isMemberOptional(members[0]) !== (required === 'last')) { + report(members[0]); + return false; + } - return false; + for (let i = switchIndex + 1; i < members.length; i++) { + if ( + i > switchIndex && + isMemberOptional(members[i]) !== + isMemberOptional(members[switchIndex]) + ) { + report(members[switchIndex]); + return false; + } + } + + return true; } /** @@ -784,8 +796,6 @@ export default util.createRule({ let memberTypes: string | MemberType[] | undefined; let required: 'first' | 'last' | undefined; - const memberSets: Array = []; - // returns true if everything is good and false if an error was reported const checkOrder = (memberSet: Member[]): boolean => { const hasAlphaSort = !!(order && order !== 'as-written'); @@ -823,55 +833,23 @@ export default util.createRule({ required = orderConfig.required; } - if (!checkRequiredOrder(members, required)) { + if (!required) { + checkOrder(members); return; } - if (required === 'first') { - // if the order of required and optional elements is correct, - // then check for correct sort and group order within the required and - // optional member sets - const firstOptionalMemberIndex = members.findIndex(member => - isMemberOptional(member), - ); - const lastRequiredMemberIndex = util.findLastIndex( - members, - m => !isMemberOptional(m), - ); - - if (firstOptionalMemberIndex != -1) { - memberSets.push(members.slice(firstOptionalMemberIndex)); - } - - if (lastRequiredMemberIndex != -1) { - memberSets.push(members.slice(0, lastRequiredMemberIndex + 1)); - } - } else if (required === 'last') { - // if the order of required and optional elements is correct, - // then check for correct order within the required and - // optional member sets - const firstRequiredMemberIndex = members.findIndex( - member => !isMemberOptional(member), - ); - const lastOptionalMemberIndex = util.findLastIndex( - members, - isMemberOptional, - ); + const switchIndex = members.findIndex( + (member, i) => + i && isMemberOptional(member) !== isMemberOptional(members[i - 1]), + ); - if (firstRequiredMemberIndex != -1) { - memberSets.push(members.slice(firstRequiredMemberIndex)); - } - - if (lastOptionalMemberIndex != -1) { - memberSets.push(members.slice(0, lastOptionalMemberIndex + 1)); + if (switchIndex !== -1) { + if (!checkRequiredOrder(members, required)) { + return; } + checkOrder(members.slice(0, switchIndex)); + checkOrder(members.slice(switchIndex)); } - - if (memberSets.length === 0) { - memberSets.push(members); - } - - memberSets.every(checkOrder); } return { diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts index bf5389157f55..fd7cc96dffd2 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts @@ -232,6 +232,39 @@ interface X { }, ], }, + { + code: ` +class Test { + a?: string; + b?: string; + f: string; + c?: string; + d?: string; + g: string; + h: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'last', + }, + }, + ], + errors: [ + { + messageId: 'incorrectRequiredMembersOrder', + line: 5, + column: 3, + data: { + member: 'f', + optionalOrRequired: 'optional', + }, + }, + ], + }, ], }; From abf6b8d9e817878940cea1daed1b5fb3e4dc6762 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Fri, 18 Nov 2022 18:31:24 +0500 Subject: [PATCH 10/14] fix(eslint-plugin): [member-ordering] refactoring for PR. --- packages/eslint-plugin/src/rules/member-ordering.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 914604b41ca7..6045f4ff3d6d 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -763,9 +763,8 @@ export default util.createRule({ for (let i = switchIndex + 1; i < members.length; i++) { if ( - i > switchIndex && isMemberOptional(members[i]) !== - isMemberOptional(members[switchIndex]) + isMemberOptional(members[switchIndex]) ) { report(members[switchIndex]); return false; From f5833990ecb2820a68d8cc23a03238446b295845 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Sat, 19 Nov 2022 00:24:20 +0500 Subject: [PATCH 11/14] fix(eslint-plugin): [member-ordering] adding test cases for coverage and removing unused code. --- .../member-ordering-required.test.ts | 31 +++++++++++++++++++ .../eslint-plugin/tests/util/misc.test.ts | 10 ++++++ 2 files changed, 41 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts index fd7cc96dffd2..fc51af2c2f84 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts @@ -265,6 +265,37 @@ class Test { }, ], }, + { + code: ` +class Test { + a: string; + b: string; + f?: string; + c?: string; + d?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'last', + }, + }, + ], + errors: [ + { + messageId: 'incorrectRequiredMembersOrder', + line: 3, + column: 3, + data: { + member: 'a', + optionalOrRequired: 'optional', + }, + }, + ], + }, ], }; diff --git a/packages/eslint-plugin/tests/util/misc.test.ts b/packages/eslint-plugin/tests/util/misc.test.ts index c4291f00d9f4..9de0827ce839 100644 --- a/packages/eslint-plugin/tests/util/misc.test.ts +++ b/packages/eslint-plugin/tests/util/misc.test.ts @@ -23,3 +23,13 @@ describe('formatWordList', () => { ); }); }); + +describe('findLastIndex', () => { + it('returns -1 if there are no elements to iterate over', () => { + expect(misc.findLastIndex([], () => true)).toBe(-1); + }); + + it('returns the index of the last element if predicate just returns true for all values', () => { + expect(misc.findLastIndex([1, 2, 3], () => true)).toBe(2); + }); +}); From 1691fa734cea49a8ec8be370fe2fad6d63303f07 Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Sat, 19 Nov 2022 01:28:46 +0500 Subject: [PATCH 12/14] fix(eslint-plugin): [member-ordering] increasing coverage to pass check. --- packages/eslint-plugin/src/rules/member-ordering.ts | 8 -------- packages/eslint-plugin/tests/util/misc.test.ts | 4 ++++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 6045f4ff3d6d..055ae1a3a63e 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -730,19 +730,11 @@ export default util.createRule({ members: Member[], required: 'first' | 'last' | undefined, ): boolean { - if (!required) { - return true; - } - const switchIndex = members.findIndex( (member, i) => i && isMemberOptional(member) !== isMemberOptional(members[i - 1]), ); - if (switchIndex === -1) { - return true; - } - const report = (member: Member): void => context.report({ messageId: 'incorrectRequiredMembersOrder', diff --git a/packages/eslint-plugin/tests/util/misc.test.ts b/packages/eslint-plugin/tests/util/misc.test.ts index 9de0827ce839..6eae810eb627 100644 --- a/packages/eslint-plugin/tests/util/misc.test.ts +++ b/packages/eslint-plugin/tests/util/misc.test.ts @@ -32,4 +32,8 @@ describe('findLastIndex', () => { it('returns the index of the last element if predicate just returns true for all values', () => { expect(misc.findLastIndex([1, 2, 3], () => true)).toBe(2); }); + + it('returns the index of the last occurance of a duplicate element', () => { + expect(misc.findLastIndex([1, 2, 3, 3, 5], n => n === 3)).toBe(3); + }); }); From bb8444f8e2092d8ebeaa0e0984a09f2a735eafbe Mon Sep 17 00:00:00 2001 From: Mahad Khan Date: Mon, 21 Nov 2022 21:06:43 +0500 Subject: [PATCH 13/14] feat(eslint-plugin): [member-ordering] adding more tests to increase coverage for isMemberOptional function. --- .../member-ordering-required.test.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts index fc51af2c2f84..6e88af70e04f 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts @@ -65,6 +65,115 @@ interface X { }, ], }, + { + code: ` +class X { + c: string; + d: string; + ['a']?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, + { + code: ` +class X { + c: string; + public static d: string; + public static ['a']?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, + { + code: ` +class X { + a: string; + static {} + b: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, + { + code: ` +class X { + a: string; + [i: number]: string; + b?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, + { + code: ` +interface X { + a: string; + [i?: number]: string; + b?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, + { + code: ` +interface X { + a: string; + (a: number): string; + new (i: number): string; + b?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'first', + }, + }, + ], + }, // required - last { code: ` @@ -120,6 +229,24 @@ interface X { }, ], }, + { + code: ` +class X { + ['c']?: string; + a: string; + b: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'alphabetically', + required: 'last', + }, + }, + ], + }, ], // required - first invalid: [ @@ -177,6 +304,35 @@ interface X { }, ], }, + { + code: ` +class X { + a?: string; + static {} + b?: string; +} + `, + options: [ + { + default: { + memberTypes: 'never', + order: 'as-written', + required: 'first', + }, + }, + ], + errors: [ + { + messageId: 'incorrectRequiredMembersOrder', + line: 3, + column: 3, + data: { + member: 'a', + optionalOrRequired: 'required', + }, + }, + ], + }, // required - last { code: ` From 832aa08a0b0a66b26d2d5baba845ad5f263ffd16 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 28 Nov 2022 10:43:49 -0500 Subject: [PATCH 14/14] Updated name to optionalityOrder --- .../docs/rules/member-ordering.md | 94 ++++++++++++++++++- .../src/rules/member-ordering.ts | 29 +++--- ...> member-ordering-optionalMembers.test.ts} | 48 +++++----- 3 files changed, 135 insertions(+), 36 deletions(-) rename packages/eslint-plugin/tests/rules/member-ordering/{member-ordering-required.test.ts => member-ordering-optionalMembers.test.ts} (86%) diff --git a/packages/eslint-plugin/docs/rules/member-ordering.md b/packages/eslint-plugin/docs/rules/member-ordering.md index 7adde7ba9a63..454463bbb390 100644 --- a/packages/eslint-plugin/docs/rules/member-ordering.md +++ b/packages/eslint-plugin/docs/rules/member-ordering.md @@ -24,6 +24,7 @@ type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; + optionalityOrder?: 'optional-first' | 'required-first'; order: | 'alphabetically' | 'alphabetically-case-insensitive' @@ -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 on one or both two levels: +The `OrderConfig` settings for each kind of construct may configure sorting on up 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 @@ -902,6 +904,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" + } + } + ] + } +} +``` + + + +##### ❌ Incorrect + +```ts +interface Foo { + a: boolean; + b?: number; + c: string; +} +``` + +##### ✅ Correct + +```ts +interface Foo { + b?: number; + a: boolean; + c: string; +} +``` + + + +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" + } + } + ] + } +} +``` + + + +##### ❌ Incorrect + +```ts +interface Foo { + a: boolean; + b?: number; + c: string; +} +``` + +##### ✅ Correct + +```ts +interface Foo { + a: boolean; + c: string; + b?: number; +} +``` + + + ## All Supported Options ### Member Types (Granular Form) diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 055ae1a3a63e..f37d36200041 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -48,13 +48,15 @@ type Order = AlphabeticalOrder | 'as-written'; interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; + optionalityOrder?: OptionalityOrder; order: Order; - required?: 'first' | 'last'; } type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; type Member = TSESTree.ClassElement | TSESTree.TypeElement; +type OptionalityOrder = 'optional-first' | 'required-first'; + export type Options = [ { default?: OrderConfig; @@ -103,9 +105,9 @@ const objectConfig = (memberTypes: MemberType[]): JSONSchema.JSONSchema4 => ({ 'natural-case-insensitive', ], }, - required: { + optionalityOrder: { type: 'string', - enum: ['first', 'last'], + enum: ['optional-first', 'required-first'], }, }, additionalProperties: false, @@ -723,12 +725,13 @@ export default util.createRule({ * 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[], - required: 'first' | 'last' | undefined, + optionalityOrder: OptionalityOrder | undefined, ): boolean { const switchIndex = members.findIndex( (member, i) => @@ -741,14 +744,18 @@ export default util.createRule({ loc: member.loc, data: { member: getMemberName(member, context.getSourceCode()), - optionalOrRequired: required === 'first' ? 'required' : 'optional', + optionalOrRequired: + optionalityOrder === 'optional-first' ? 'required' : 'optional', }, }); - // if the optionality of the first item is correct (based on required) + // 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]) !== (required === 'last')) { + if ( + isMemberOptional(members[0]) !== + (optionalityOrder === 'required-first') + ) { report(members[0]); return false; } @@ -785,7 +792,7 @@ export default util.createRule({ // Standardize config let order: Order | undefined; let memberTypes: string | MemberType[] | undefined; - let required: 'first' | 'last' | undefined; + let optionalityOrder: OptionalityOrder | undefined; // returns true if everything is good and false if an error was reported const checkOrder = (memberSet: Member[]): boolean => { @@ -821,10 +828,10 @@ export default util.createRule({ } else { order = orderConfig.order; memberTypes = orderConfig.memberTypes; - required = orderConfig.required; + optionalityOrder = orderConfig.optionalityOrder; } - if (!required) { + if (!optionalityOrder) { checkOrder(members); return; } @@ -835,7 +842,7 @@ export default util.createRule({ ); if (switchIndex !== -1) { - if (!checkRequiredOrder(members, required)) { + if (!checkRequiredOrder(members, optionalityOrder)) { return; } checkOrder(members.slice(0, switchIndex)); diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-optionalMembers.test.ts similarity index 86% rename from packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts rename to packages/eslint-plugin/tests/rules/member-ordering/member-ordering-optionalMembers.test.ts index 6e88af70e04f..9c72fc7322a9 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-required.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-optionalMembers.test.ts @@ -10,7 +10,7 @@ const ruleTester = new RuleTester({ const grouped: TSESLint.RunTests = { valid: [ - // required - first + // optionalityOrder - optional-first { code: ` interface X { @@ -24,7 +24,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -42,7 +42,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -60,7 +60,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -78,7 +78,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -96,7 +96,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -114,7 +114,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -132,7 +132,7 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -150,7 +150,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -169,12 +169,12 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], }, - // required - last + // optionalityOrder - required-first { code: ` interface X { @@ -188,7 +188,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -206,7 +206,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -224,7 +224,7 @@ interface X { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -242,13 +242,13 @@ class X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'last', + optionalityOrder: 'required-first', }, }, ], }, ], - // required - first + // optionalityOrder - optional-first invalid: [ { code: ` @@ -263,7 +263,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -288,7 +288,7 @@ interface X { default: { memberTypes: ['call-signature', 'field', 'method'], order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -317,7 +317,7 @@ class X { default: { memberTypes: 'never', order: 'as-written', - required: 'first', + optionalityOrder: 'optional-first', }, }, ], @@ -333,7 +333,7 @@ class X { }, ], }, - // required - last + // optionalityOrder - required-first { code: ` interface X { @@ -347,7 +347,7 @@ interface X { default: { memberTypes: 'never', order: 'alphabetically', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -372,7 +372,7 @@ interface X { default: { memberTypes: ['call-signature', 'field', 'method'], order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -405,7 +405,7 @@ class Test { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ], @@ -436,7 +436,7 @@ class Test { default: { memberTypes: 'never', order: 'as-written', - required: 'last', + optionalityOrder: 'required-first', }, }, ],