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', }, }, ], pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy