diff --git a/docs/content/rules/sort-sets.mdx b/docs/content/rules/sort-sets.mdx new file mode 100644 index 00000000..9853e9ed --- /dev/null +++ b/docs/content/rules/sort-sets.mdx @@ -0,0 +1,235 @@ +--- +title: sort-sets +description: Ensure your sets are sorted. Improve readability and maintain consistent code with this ESLint rule +shortDescription: Enforce sorted sets +keywords: + - eslint + - sort set + - eslint rule + - coding standards + - code quality + - javascript linting + - set sorting +--- + +import CodeExample from '../../components/CodeExample.svelte' +import CodeTabs from '../../components/CodeTabs.svelte' +import { dedent } from 'ts-dedent' + +Enforce sorted set values. + +By keeping sets sorted, developers can quickly scan and verify the values, making the code more predictable and reducing the likelihood of errors. This practice simplifies debugging and enhances the overall clarity of the codebase. + + +## Try it out + + { + let electronics = new Set([ + 'Drone', + 'Headphones', + 'Keyboard', + 'Laptop', + 'Monitor', + 'Mouse', + 'Router', + 'Smartphone', + 'Smartwatch', + 'Tablet', + ]) + let accessories = new Set([ + 'Adapter', + 'Case', + 'Charger', + 'Screen Protector', + 'Cable', + 'Battery', + 'Memory Card', + ]) + if (electronics.has(product.name)) { + return 'Electronics' + } else if (accessories.has(product.name)) { + return 'Accessories' + } + return 'Unknown' + } +`} + lineLength={dedent` + const getProductCategories = (product) => { + let electronics = new Set([ + 'Smartphone', + 'Smartwatch', + 'Headphones', + 'Keyboard', + 'Monitor', + 'Laptop', + 'Router', + 'Tablet', + 'Drone', + 'Mouse', + ]) + let accessories = new Set([ + 'Screen Protector', + 'Memory Card', + 'Adapter', + 'Charger', + 'Battery', + 'Cable', + 'Case', + ]) + if (electronics.has(product.name)) { + return 'Electronics' + } else if (accessories.has(product.name)) { + return 'Accessories' + } + return 'Unknown' + } +`} + client:load + lang="tsx" + initial={dedent` + const getProductCategories = (product) => { + let electronics = new Set([ + 'Mouse', + 'Drone', + 'Smartphone', + 'Keyboard', + 'Tablet', + 'Monitor', + 'Laptop', + 'Smartwatch', + 'Router', + 'Headphones', + ]) + let accessories = new Set([ + 'Memory Card', + 'Charger', + 'Cable', + 'Battery', + 'Screen Protector', + 'Case', + 'Adapter', + ]) + if (electronics.has(product.name)) { + return 'Electronics' + } else if (accessories.has(product.name)) { + return 'Accessories' + } + return 'Unknown' + } +`} +/> + +## Options + +This rule accepts an options object with the following properties: + +### type + +default: `'alphabetical'` + +Specifies the sorting method. + +- `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”). +- `'natural'` — Sort items in a natural order (e.g., “item2” < “item10”). +- `'line-length'` — Sort items by the length of the code line (shorter lines first). + +### order + +default: `'asc'` + +Determines whether the sorted items should be in ascending or descending order. + +- `'asc'` — Sort items in ascending order (A to Z, 1 to 9). +- `'desc'` — Sort items in descending order (Z to A, 9 to 1). + +### ignoreCase + +default: `true` + +Controls whether sorting should be case-sensitive or not. + +- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same). +- `false` — Consider case when sorting (e.g., “A” comes before “a”). + + +### groupKind + +default: `'literals-first'` + +Allows you to group set elements by their kind, determining whether spread values should come before or after literal values. + +- `mixed` — Do not group set elements by their kind; spread values are sorted together with literal values. +- `literals-first` — Group all literal values before spread values. +- `spread-first` — Group all spread values before literal values. + + +## Usage + + + +## Version + +This rule was introduced in [v3.4.0](https://github.com/azat-io/eslint-plugin-perfectionist/releases/tag/v3.4.0). + +## Resources + +- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-sets.ts) +- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-sets.test.ts) diff --git a/index.ts b/index.ts index 0e39d8f0..90fa1e84 100644 --- a/index.ts +++ b/index.ts @@ -22,6 +22,7 @@ import sortExports from './rules/sort-exports' import sortObjects from './rules/sort-objects' import sortEnums from './rules/sort-enums' import sortMaps from './rules/sort-maps' +import sortSets from './rules/sort-sets' interface BaseOptions { type: 'alphabetical' | 'line-length' | 'natural' @@ -54,6 +55,7 @@ let plugin = { 'sort-exports': sortExports, 'sort-objects': sortObjects, 'sort-enums': sortEnums, + 'sort-sets': sortSets, 'sort-maps': sortMaps, }, name, diff --git a/rules/sort-array-includes.ts b/rules/sort-array-includes.ts index 4a7488cf..76068206 100644 --- a/rules/sort-array-includes.ts +++ b/rules/sort-array-includes.ts @@ -1,3 +1,5 @@ +import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint' import type { TSESTree } from '@typescript-eslint/types' import type { SortingNode } from '../typings' @@ -17,7 +19,7 @@ import { compare } from '../utils/compare' type MESSAGE_ID = 'unexpectedArrayIncludesOrder' -type Options = [ +export type Options = [ Partial<{ groupKind: 'literals-first' | 'spreads-first' | 'mixed' type: 'alphabetical' | 'line-length' | 'natural' @@ -26,6 +28,33 @@ type Options = [ }>, ] +export let jsonSchema: JSONSchema4 = { + type: 'object', + properties: { + type: { + description: 'Specifies the sorting method.', + type: 'string', + enum: ['alphabetical', 'natural', 'line-length'], + }, + order: { + description: + 'Determines whether the sorted items should be in ascending or descending order.', + type: 'string', + enum: ['asc', 'desc'], + }, + ignoreCase: { + description: 'Controls whether sorting should be case-sensitive or not.', + type: 'boolean', + }, + groupKind: { + description: 'Specifies top-level groups.', + enum: ['mixed', 'literals-first', 'spreads-first'], + type: 'string', + }, + }, + additionalProperties: false, +} + export default createEslintRule({ name: 'sort-array-includes', meta: { @@ -34,35 +63,7 @@ export default createEslintRule({ description: 'Enforce sorted arrays before include method.', }, fixable: 'code', - schema: [ - { - type: 'object', - properties: { - type: { - description: 'Specifies the sorting method.', - type: 'string', - enum: ['alphabetical', 'natural', 'line-length'], - }, - order: { - description: - 'Determines whether the sorted items should be in ascending or descending order.', - type: 'string', - enum: ['asc', 'desc'], - }, - ignoreCase: { - description: - 'Controls whether sorting should be case-sensitive or not.', - type: 'boolean', - }, - groupKind: { - description: 'Specifies top-level groups.', - enum: ['mixed', 'literals-first', 'spreads-first'], - type: 'string', - }, - }, - additionalProperties: false, - }, - ], + schema: [jsonSchema], messages: { unexpectedArrayIncludesOrder: 'Expected "{{right}}" to come before "{{left}}".', @@ -88,89 +89,95 @@ export default createEslintRule({ node.object.type === 'ArrayExpression' ? node.object.elements : node.object.arguments + sortArray(context, 'unexpectedArrayIncludesOrder', elements) + } + }, + }), +}) - let settings = getSettings(context.settings) +export let sortArray = ( + context: Readonly>, + messageId: MessageIds, + elements: (TSESTree.SpreadElement | TSESTree.Expression | null)[], +) => { + let settings = getSettings(context.settings) - if (elements.length > 1) { - let options = complete(context.options.at(0), settings, { - groupKind: 'literals-first', - type: 'alphabetical', - ignoreCase: true, - order: 'asc', - } as const) + if (elements.length > 1) { + let options = complete(context.options.at(0), settings, { + groupKind: 'literals-first', + type: 'alphabetical', + ignoreCase: true, + order: 'asc', + } as const) - let sourceCode = getSourceCode(context) - let nodes: ({ type: string } & SortingNode)[] = elements - .reduce( - ( - accumulator: ({ type: string } & SortingNode)[][], - element: TSESTree.SpreadElement | TSESTree.Expression | null, - ) => { - if (element !== null) { - let group = 'unknown' - if (typeof options.groupKind === 'string') { - group = - element.type === 'SpreadElement' ? 'spread' : 'literal' - } - accumulator.at(0)!.push({ - name: - element.type === 'Literal' - ? `${element.value}` - : sourceCode.text.slice(...element.range), - size: rangeToDiff(element.range), - type: element.type, - node: element, - group, - }) - } + let sourceCode = getSourceCode(context) + let nodes: ({ type: string } & SortingNode)[] = elements + .reduce( + ( + accumulator: ({ type: string } & SortingNode)[][], + element: TSESTree.SpreadElement | TSESTree.Expression | null, + ) => { + if (element !== null) { + let group = 'unknown' + if (typeof options.groupKind === 'string') { + group = element.type === 'SpreadElement' ? 'spread' : 'literal' + } + accumulator.at(0)!.push({ + name: + element.type === 'Literal' + ? `${element.value}` + : sourceCode.text.slice(...element.range), + size: rangeToDiff(element.range), + type: element.type, + node: element, + group, + }) + } - return accumulator - }, - [[], []], - ) - .flat() + return accumulator + }, + [[], []], + ) + .flat() - pairwise(nodes, (left, right) => { - let groupKindOrder = ['unknown'] + pairwise(nodes, (left, right) => { + let groupKindOrder = ['unknown'] - if (typeof options.groupKind === 'string') { - groupKindOrder = - options.groupKind === 'literals-first' - ? ['literal', 'spread'] - : ['spread', 'literal'] - } + if (typeof options.groupKind === 'string') { + groupKindOrder = + options.groupKind === 'literals-first' + ? ['literal', 'spread'] + : ['spread', 'literal'] + } - let leftNum = getGroupNumber(groupKindOrder, left) - let rightNum = getGroupNumber(groupKindOrder, right) + let leftNum = getGroupNumber(groupKindOrder, left) + let rightNum = getGroupNumber(groupKindOrder, right) - if ( - (options.groupKind !== 'mixed' && leftNum > rightNum) || - ((options.groupKind === 'mixed' || leftNum === rightNum) && - isPositive(compare(left, right, options))) - ) { - context.report({ - messageId: 'unexpectedArrayIncludesOrder', - data: { - left: toSingleLine(left.name), - right: toSingleLine(right.name), - }, - node: right.node, - fix: fixer => { - let sortedNodes = - options.groupKind !== 'mixed' - ? groupKindOrder - .map(group => nodes.filter(n => n.group === group)) - .map(groupedNodes => sortNodes(groupedNodes, options)) - .flat() - : sortNodes(nodes, options) + if ( + (options.groupKind !== 'mixed' && leftNum > rightNum) || + ((options.groupKind === 'mixed' || leftNum === rightNum) && + isPositive(compare(left, right, options))) + ) { + context.report({ + messageId, + data: { + left: toSingleLine(left.name), + right: toSingleLine(right.name), + }, + node: right.node, + fix: fixer => { + let sortedNodes = + options.groupKind !== 'mixed' + ? groupKindOrder + .map(group => nodes.filter(n => n.group === group)) + .map(groupedNodes => sortNodes(groupedNodes, options)) + .flat() + : sortNodes(nodes, options) - return makeFixes(fixer, nodes, sortedNodes, sourceCode) - }, - }) - } - }) - } + return makeFixes(fixer, nodes, sortedNodes, sourceCode) + }, + }) } - }, - }), -}) + }) + } +} diff --git a/rules/sort-sets.ts b/rules/sort-sets.ts new file mode 100644 index 00000000..d1f52d37 --- /dev/null +++ b/rules/sort-sets.ts @@ -0,0 +1,48 @@ +import type { Options } from './sort-array-includes' + +import { createEslintRule } from '../utils/create-eslint-rule' +import { jsonSchema, sortArray } from './sort-array-includes' + +type MESSAGE_ID = 'unexpectedSetsOrder' + +export default createEslintRule({ + name: 'sort-sets', + meta: { + type: 'suggestion', + docs: { + description: 'Enforce sorted sets.', + }, + fixable: 'code', + schema: [jsonSchema], + messages: { + unexpectedSetsOrder: 'Expected "{{right}}" to come before "{{left}}".', + }, + }, + defaultOptions: [ + { + type: 'alphabetical', + order: 'asc', + ignoreCase: true, + groupKind: 'literals-first', + }, + ], + create: context => ({ + NewExpression: node => { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'Set' && + node.arguments.length && + (node.arguments[0]?.type === 'ArrayExpression' || + (node.arguments[0]?.type === 'NewExpression' && + 'name' in node.arguments[0].callee && + node.arguments[0].callee.name === 'Array')) + ) { + let elements = + node.arguments[0].type === 'ArrayExpression' + ? node.arguments[0].elements + : node.arguments[0].arguments + sortArray(context, 'unexpectedSetsOrder', elements) + } + }, + }), +}) diff --git a/test/sort-sets.test.ts b/test/sort-sets.test.ts new file mode 100644 index 00000000..e342d4b1 --- /dev/null +++ b/test/sort-sets.test.ts @@ -0,0 +1,959 @@ +import { RuleTester } from '@typescript-eslint/rule-tester' +import { afterAll, describe, it } from 'vitest' +import { dedent } from 'ts-dedent' + +import rule from '../rules/sort-sets' + +let ruleName = 'sort-sets' + +describe(ruleName, () => { + RuleTester.describeSkip = describe.skip + RuleTester.afterAll = afterAll + RuleTester.describe = describe + RuleTester.itOnly = it.only + RuleTester.itSkip = it.skip + RuleTester.it = it + + let ruleTester = new RuleTester() + + describe(`${ruleName}: sorting by alphabetical order`, () => { + let type = 'alphabetical-order' + + let options = { + type: 'alphabetical', + ignoreCase: true, + order: 'asc', + } as const + + ruleTester.run( + `${ruleName}(${type}): does not break the property list`, + rule, + { + valid: [ + { + code: dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + 'e', + ...other, + ]) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + 'a', + 'c', + 'b', + 'd', + 'e', + ...other, + ]) + `, + output: dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + 'e', + ...other, + ]) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'c', + right: 'b', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts spread elements`, rule, { + valid: [ + { + code: dedent` + new Set([ + ...aaa, + ...bbbb, + ...ccc, + ]) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + ...aaa, + ...ccc, + ...bbbb, + ]) + `, + output: dedent` + new Set([ + ...aaa, + ...bbbb, + ...ccc, + ]) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...ccc', + right: '...bbbb', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): ignores nullable array elements`, + rule, + { + valid: [ + { + code: dedent` + new Set(['a', 'b', 'c',, 'd']) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set(['b', 'a', 'c',, 'd']) + `, + output: dedent` + new Set(['a', 'b', 'c',, 'd']) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allow to put spread elements to the end`, + rule, + { + valid: [ + { + code: dedent` + new Set(['a', 'b', 'c', ...other]) + `, + options: [ + { + ...options, + groupKind: 'literals-first', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set(['a', 'b', ...other, 'c']) + `, + output: dedent` + new Set(['a', 'b', 'c', ...other]) + `, + options: [ + { + ...options, + groupKind: 'literals-first', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...other', + right: 'c', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts array constructor`, rule, { + valid: [ + { + code: dedent` + new Set(new Array( + 'a', + 'b', + 'c', + 'd', + )) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set(new Array( + 'a', + 'c', + 'b', + 'd', + )) + `, + output: dedent` + new Set(new Array( + 'a', + 'b', + 'c', + 'd', + )) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'c', + right: 'b', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${ruleName}(${type}): allows mixed sorting`, rule, { + valid: [ + { + code: dedent` + new Set(new Array( + ...d, + 'aaaa', + 'bbb', + 'cc', + )) + `, + options: [ + { + ...options, + groupKind: 'mixed', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set(new Array( + 'aaaa', + 'bbb', + ...d, + 'cc', + )) + `, + output: dedent` + new Set(new Array( + ...d, + 'aaaa', + 'bbb', + 'cc', + )) + `, + options: [ + { + ...options, + groupKind: 'mixed', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'bbb', + right: '...d', + }, + }, + ], + }, + ], + }) + }) + + describe(`${ruleName}: sorting by natural order`, () => { + let type = 'natural-order' + + let options = { + ignoreCase: true, + type: 'natural', + order: 'asc', + } as const + + ruleTester.run( + `${ruleName}(${type}): does not break the property list`, + rule, + { + valid: [ + { + code: dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + 'e', + ...other, + ]) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + 'a', + 'c', + 'b', + 'd', + 'e', + ...other, + ]) + `, + output: dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + 'e', + ...other, + ]) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'c', + right: 'b', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts spread elements`, rule, { + valid: [ + { + code: dedent` + new Set([ + ...aaa, + ...bbbb, + ...ccc, + ]) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + ...aaa, + ...ccc, + ...bbbb, + ]) + `, + output: dedent` + new Set([ + ...aaa, + ...bbbb, + ...ccc, + ]) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...ccc', + right: '...bbbb', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): ignores nullable array elements`, + rule, + { + valid: [ + { + code: dedent` + new Set(['a', 'b', 'c',, 'd']) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set(['b', 'a', 'c',, 'd']) + `, + output: dedent` + new Set(['a', 'b', 'c',, 'd']) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allow to put spread elements to the end`, + rule, + { + valid: [ + { + code: dedent` + new Set(['a', 'b', 'c', ...other]) + `, + options: [ + { + ...options, + groupKind: 'literals-first', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set(['a', 'b', ...other, 'c']) + `, + output: dedent` + new Set(['a', 'b', 'c', ...other]) + `, + options: [ + { + ...options, + groupKind: 'literals-first', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...other', + right: 'c', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts array constructor`, rule, { + valid: [ + { + code: dedent` + new Set(new Array( + 'a', + 'b', + 'c', + 'd', + )) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set(new Array( + 'a', + 'c', + 'b', + 'd', + )) + `, + output: dedent` + new Set(new Array( + 'a', + 'b', + 'c', + 'd', + )) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'c', + right: 'b', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${ruleName}(${type}): allows mixed sorting`, rule, { + valid: [ + { + code: dedent` + new Set(new Array( + ...d, + 'aaaa', + 'bbb', + 'cc', + )) + `, + options: [ + { + ...options, + groupKind: 'mixed', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set(new Array( + 'aaaa', + 'bbb', + ...d, + 'cc', + )) + `, + output: dedent` + new Set(new Array( + ...d, + 'aaaa', + 'bbb', + 'cc', + )) + `, + options: [ + { + ...options, + groupKind: 'mixed', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'bbb', + right: '...d', + }, + }, + ], + }, + ], + }) + }) + + describe(`${ruleName}: sorting by line length`, () => { + let type = 'line-length-order' + + let options = { + type: 'line-length', + order: 'desc', + } as const + + ruleTester.run( + `${ruleName}(${type}): does not break the property list`, + rule, + { + valid: [ + { + code: dedent` + new Set([ + 'aaaaa', + 'bbbb', + 'ccc', + 'dd', + 'e', + ...other, + ]) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + 'aaaaa', + 'ccc', + 'bbbb', + 'dd', + 'e', + ...other, + ]) + `, + output: dedent` + new Set([ + 'aaaaa', + 'bbbb', + 'ccc', + 'dd', + 'e', + ...other, + ]) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'ccc', + right: 'bbbb', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts spread elements`, rule, { + valid: [ + { + code: dedent` + new Set([ + ...bbbb, + ...aaa, + ...ccc, + ]) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + ...aaa, + ...bbbb, + ...ccc, + ]) + `, + output: dedent` + new Set([ + ...bbbb, + ...aaa, + ...ccc, + ]) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...aaa', + right: '...bbbb', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): ignores nullable array elements`, + rule, + { + valid: [ + { + code: dedent` + new Set(['a', 'b', 'c',, 'd']) + `, + options: [options], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allow to put spread elements to the end`, + rule, + { + valid: [ + { + code: dedent` + new Set(['a', 'b', 'c', ...other]) + `, + options: [ + { + ...options, + groupKind: 'literals-first', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set(['a', 'b', ...other, 'c']) + `, + output: dedent` + new Set(['a', 'b', 'c', ...other]) + `, + options: [ + { + ...options, + groupKind: 'literals-first', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...other', + right: 'c', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts array constructor`, rule, { + valid: [ + { + code: dedent` + new Set(new Array( + 'aaaa', + 'bbb', + 'cc', + 'd', + )) + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + new Set(new Array( + 'aaaa', + 'cc', + 'bbb', + 'd', + )) + `, + output: dedent` + new Set(new Array( + 'aaaa', + 'bbb', + 'cc', + 'd', + )) + `, + options: [options], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'cc', + right: 'bbb', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${ruleName}(${type}): allows mixed sorting`, rule, { + valid: [ + { + code: dedent` + new Set(new Array( + 'aaaa', + 'bbb', + ...d, + 'cc', + )) + `, + options: [ + { + ...options, + groupKind: 'mixed', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set(new Array( + 'aaaa', + ...d, + 'bbb', + 'cc', + )) + `, + output: dedent` + new Set(new Array( + 'aaaa', + 'bbb', + ...d, + 'cc', + )) + `, + options: [ + { + ...options, + groupKind: 'mixed', + }, + ], + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: '...d', + right: 'bbb', + }, + }, + ], + }, + ], + }) + }) + + describe(`${ruleName}: misc`, () => { + ruleTester.run( + `${ruleName}: sets alphabetical asc sorting as default`, + rule, + { + valid: [ + dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + ]) + `, + { + code: dedent` + new Set([ + 'v1.png', + 'v10.png', + 'v12.png', + 'v2.png', + ]) + `, + options: [ + { + ignoreCase: false, + }, + ], + }, + ], + invalid: [ + { + code: dedent` + new Set([ + 'b', + 'a', + 'd', + 'c', + ]) + `, + output: dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + ]) + `, + errors: [ + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'b', + right: 'a', + }, + }, + { + messageId: 'unexpectedSetsOrder', + data: { + left: 'd', + right: 'c', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}: works consistently with an empty array or an array with one element`, + rule, + { + valid: ['new Set([])', "new Set(['a'])"], + invalid: [], + }, + ) + + ruleTester.run(`${ruleName}: ignores quotes of strings`, rule, { + valid: [ + dedent` + new Set(['a', "b", 'c']) + `, + ], + invalid: [], + }) + }) +})