diff --git a/docs/content/rules/sort-array-includes.mdx b/docs/content/rules/sort-array-includes.mdx index 2b3d1056a..4c1b7b944 100644 --- a/docs/content/rules/sort-array-includes.mdx +++ b/docs/content/rules/sort-array-includes.mdx @@ -129,7 +129,7 @@ Specifies the sorting method. - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). - `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. -- `'unsorted'` — Do not sort items. To be used with the [`useConfigurationIf`](#useConfigurationIf) option. +- `'unsorted'` — Do not sort items. To be used with the [`useConfigurationIf`](#useconfigurationif) option. ### order diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx index 4947477ec..ffde4e7ea 100644 --- a/docs/content/rules/sort-interfaces.mdx +++ b/docs/content/rules/sort-interfaces.mdx @@ -143,6 +143,7 @@ Specifies the sorting method. - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). - `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. +- `'unsorted'` — Do not sort items. To be used with the [`useConfigurationIf`](#useconfigurationif) option. ### order @@ -191,10 +192,12 @@ Specifies the sorting locales. See [String.prototype.localeCompare() - locales]( - `string` — A BCP 47 language tag (e.g. `'en'`, `'en-US'`, `'zh-CN'`). - `string[]` — An array of BCP 47 language tags. -### ignorePattern +### [DEPRECATED] ignorePattern default: `[]` +Use the [useConfigurationIf.declarationMatchesPattern](#useconfigurationif) option alongside [type: unsorted](#type) instead. + Allows you to specify names or patterns for interfaces that should be ignored by this rule. This can be useful if you have specific interfaces that you do not want to sort. You can specify their names or a regexp pattern to ignore, for example: `'^Component.+'` to ignore all interfaces whose names begin with the word “Component”. @@ -258,6 +261,62 @@ Specifies how optional and required members should be ordered in TypeScript inte - `'required-first'` — Put all required members before optional members. - `'mixed'` — Do not enforce any specific order based on optionality. +### useConfigurationIf + + + type: `{ allNamesMatchPattern?: string; declarationMatchesPattern?: string }` + +default: `{}` + +Allows you to specify filters to match a particular options configuration for a given interface. + +The first matching options configuration will be used. If no configuration matches, the default options configuration will be used. + +- `allNamesMatchPattern` — A regexp pattern that all keys must match. + +Example configuration: +```ts +{ + 'perfectionist/sort-interfaces': [ + 'error', + { + groups: ['r', 'g', 'b'], // Sort colors types by RGB + customGroups: { + r: '^r$', + g: '^g$', + b: '^b$', + }, + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + }, + { + type: 'alphabetical' // Fallback configuration + } + ], +} +``` + +- `declarationMatchesPattern` — A regexp pattern that the interface declaration must match. + +Example configuration: +```ts +{ + 'perfectionist/sort-interfaces': [ + 'error', + { + type: 'unsorted', // Do not sort Metadata interfaces + useConfigurationIf: { + declarationMatchesPattern: '*Metadata$', + }, + }, + { + type: 'alphabetical' // Fallback configuration + } + ], +} +``` + ### groups diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx index 4ee7dbcf6..7c15e6340 100644 --- a/docs/content/rules/sort-object-types.mdx +++ b/docs/content/rules/sort-object-types.mdx @@ -105,6 +105,7 @@ Specifies the sorting method. - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). - `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. +- `'unsorted'` — Do not sort items. To be used with the [`useConfigurationIf`](#useconfigurationif) option. ### order @@ -153,10 +154,12 @@ Specifies the sorting locales. See [String.prototype.localeCompare() - locales]( - `string` — A BCP 47 language tag (e.g. `'en'`, `'en-US'`, `'zh-CN'`). - `string[]` — An array of BCP 47 language tags. -### ignorePattern +### [DEPRECATED] ignorePattern default: `[]` +Use the [useConfigurationIf.declarationMatchesPattern](#useconfigurationif) option alongside [type: unsorted](#type) instead. + Allows you to specify names or patterns for object types that should be ignored by this rule. This can be useful if you have specific object types that you do not want to sort. You can specify their names or a regexp pattern to ignore, for example: `'^Component.+'` to ignore all object types whose names begin with the word “Component”. @@ -223,6 +226,62 @@ Allows you to group type object keys by their kind, determining whether required - `required-first` — Group all required values before optional. - `optional-first` — Group all optional values before required. +### useConfigurationIf + + + type: `{ allNamesMatchPattern?: string; declarationMatchesPattern?: string }` + +default: `{}` + +Allows you to specify filters to match a particular options configuration for a given object type. + +The first matching options configuration will be used. If no configuration matches, the default options configuration will be used. + +- `allNamesMatchPattern` — A regexp pattern that all keys must match. + +Example configuration: +```ts +{ + 'perfectionist/sort-object-types': [ + 'error', + { + groups: ['r', 'g', 'b'], // Sort colors types by RGB + customGroups: { + r: '^r$', + g: '^g$', + b: '^b$', + }, + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + }, + { + type: 'alphabetical' // Fallback configuration + } + ], +} +``` + +- `declarationMatchesPattern` — A regexp pattern that the object type declaration must match. + +Example configuration: +```ts +{ + 'perfectionist/sort-object-types': [ + 'error', + { + type: 'unsorted', // Do not sort Metadata types + useConfigurationIf: { + declarationMatchesPattern: '*Metadata$', + }, + }, + { + type: 'alphabetical' // Fallback configuration + } + ], +} +``` + ### groups diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx index 369d6021c..e6f6247fa 100644 --- a/docs/content/rules/sort-objects.mdx +++ b/docs/content/rules/sort-objects.mdx @@ -163,7 +163,7 @@ Specifies the sorting method. - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). - `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. -- `'unsorted'` — Do not sort items. To be used with the [`useConfigurationIf`](#useConfigurationIf) option. +- `'unsorted'` — Do not sort items. To be used with the [`useConfigurationIf`](#useconfigurationif) option. ### order diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts index 6267fcf9f..9a33ea44f 100644 --- a/rules/sort-interfaces.ts +++ b/rules/sort-interfaces.ts @@ -16,6 +16,7 @@ let defaultOptions: Required = { partitionByNewLine: false, newlinesBetween: 'ignore', specialCharacters: 'keep', + useConfigurationIf: {}, type: 'alphabetical', groupKind: 'mixed', ignorePattern: [], @@ -44,7 +45,7 @@ export default createEslintRule({ description: 'Enforce sorted interface properties.', recommended: true, }, - schema: [jsonSchema], + schema: jsonSchema, type: 'suggestion', fixable: 'code', }, diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts index 18e3506bd..b62a8a545 100644 --- a/rules/sort-object-types.ts +++ b/rules/sort-object-types.ts @@ -1,11 +1,13 @@ 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 { TSESLint } from '@typescript-eslint/utils' import type { Modifier, Selector, Options } from './sort-object-types.types' import type { SortingNode } from '../typings' import { + buildUseConfigurationIfJsonSchema, buildCustomGroupsArrayJsonSchema, partitionByCommentJsonSchema, partitionByNewLineJsonSchema, @@ -23,6 +25,7 @@ import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-new import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration' import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getCustomGroupsCompareOptions } from '../utils/get-custom-groups-compare-options' +import { getMatchingContextOptions } from '../utils/get-matching-context-options' import { generatePredefinedGroups } from '../utils/generate-predefined-groups' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { singleCustomGroupJsonSchema } from './sort-object-types.types' @@ -70,6 +73,7 @@ let defaultOptions: Required = { partitionByNewLine: false, newlinesBetween: 'ignore', specialCharacters: 'keep', + useConfigurationIf: {}, type: 'alphabetical', groupKind: 'mixed', ignorePattern: [], @@ -82,43 +86,54 @@ let defaultOptions: Required = { } export let jsonSchema: JSONSchema4 = { - properties: { - ignorePattern: { - description: - 'Specifies names or patterns for nodes that should be ignored by rule.', - items: { + items: { + properties: { + ignorePattern: { + description: + 'Specifies names or patterns for nodes that should be ignored by rule.', + items: { + type: 'string', + }, + type: 'array', + }, + useConfigurationIf: buildUseConfigurationIfJsonSchema({ + additionalProperties: { + declarationMatchesPattern: { + type: 'string', + }, + }, + }), + partitionByComment: { + ...partitionByCommentJsonSchema, + description: + 'Allows you to use comments to separate members into logical groups.', + }, + customGroups: { + oneOf: [ + customGroupsJsonSchema, + buildCustomGroupsArrayJsonSchema({ singleCustomGroupJsonSchema }), + ], + }, + groupKind: { + enum: ['mixed', 'required-first', 'optional-first'], + description: 'Specifies top-level groups.', type: 'string', }, - type: 'array', - }, - partitionByComment: { - ...partitionByCommentJsonSchema, - description: - 'Allows you to use comments to separate members into logical groups.', + type: buildTypeJsonSchema({ withUnsorted: true }), + partitionByNewLine: partitionByNewLineJsonSchema, + specialCharacters: specialCharactersJsonSchema, + newlinesBetween: newlinesBetweenJsonSchema, + ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, + locales: localesJsonSchema, + groups: groupsJsonSchema, + order: orderJsonSchema, }, - customGroups: { - oneOf: [ - customGroupsJsonSchema, - buildCustomGroupsArrayJsonSchema({ singleCustomGroupJsonSchema }), - ], - }, - groupKind: { - enum: ['mixed', 'required-first', 'optional-first'], - description: 'Specifies top-level groups.', - type: 'string', - }, - partitionByNewLine: partitionByNewLineJsonSchema, - specialCharacters: specialCharactersJsonSchema, - newlinesBetween: newlinesBetweenJsonSchema, - ignoreCase: ignoreCaseJsonSchema, - alphabet: alphabetJsonSchema, - type: buildTypeJsonSchema(), - locales: localesJsonSchema, - groups: groupsJsonSchema, - order: orderJsonSchema, + additionalProperties: false, + type: 'object', }, - additionalProperties: false, - type: 'object', + uniqueItems: true, + type: 'array', } export default createEslintRule({ @@ -138,7 +153,7 @@ export default createEslintRule({ description: 'Enforce sorted object types.', recommended: true, }, - schema: [jsonSchema], + schema: jsonSchema, type: 'suggestion', fixable: 'code', }, @@ -184,7 +199,37 @@ export let sortObjectTypeElements = ({ } let settings = getSettings(context.settings) - let options = complete(context.options.at(0), settings, defaultOptions) + let sourceCode = getSourceCode(context) + let matchedContextOptions = getMatchingContextOptions({ + nodeNames: elements.map(node => + getNodeName({ typeElement: node, sourceCode }), + ), + contextOptions: context.options, + }).find(options => { + if (!options.useConfigurationIf?.declarationMatchesPattern) { + return true + } + if (!parentNodeName) { + return false + } + return matches( + parentNodeName, + options.useConfigurationIf.declarationMatchesPattern, + ) + }) + let completeOptions = complete( + matchedContextOptions, + settings, + defaultOptions, + ) + let { type } = completeOptions + if (type === 'unsorted') { + return + } + let options = { + ...completeOptions, + type, + } validateCustomSortConfiguration(options) validateGeneratedGroupsConfiguration({ customGroups: options.customGroups, @@ -202,7 +247,6 @@ export let sortObjectTypeElements = ({ return } - let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, sourceCode, @@ -218,44 +262,10 @@ export let sortObjectTypeElements = ({ return accumulator } - let name: string let lastSortingNode = accumulator.at(-1)?.at(-1) let { setCustomGroups, defineGroup, getGroup } = useGroups(options) - let formatName = (value: string): string => value.replace(/[,;]$/u, '') - - if (typeElement.type === 'TSPropertySignature') { - if (typeElement.key.type === 'Identifier') { - ;({ name } = typeElement.key) - } else if (typeElement.key.type === 'Literal') { - name = `${typeElement.key.value}` - } else { - let end: number = - typeElement.typeAnnotation?.range.at(0) ?? - typeElement.range.at(1)! - (typeElement.optional ? '?'.length : 0) - name = sourceCode.text.slice(typeElement.range.at(0), end) - } - } else if (typeElement.type === 'TSIndexSignature') { - let endIndex: number = - typeElement.typeAnnotation?.range.at(0) ?? typeElement.range.at(1)! - - name = formatName( - sourceCode.text.slice(typeElement.range.at(0), endIndex), - ) - } else if ('name' in typeElement.key) { - // TSMethodSignature - ;({ name } = typeElement.key) - /* v8 ignore next 8 - Unsure if we can reach it */ - } else { - name = formatName( - sourceCode.text.slice( - typeElement.range.at(0), - typeElement.range.at(1), - ), - ) - } - let selectors: Selector[] = [] let modifiers: Modifier[] = [] @@ -295,6 +305,7 @@ export let sortObjectTypeElements = ({ defineGroup(predefinedGroup) } + let name = getNodeName({ typeElement, sourceCode }) if (Array.isArray(options.customGroups)) { for (let customGroup of options.customGroups) { if ( @@ -450,3 +461,44 @@ export let sortObjectTypeElements = ({ }) } } + +let getNodeName = ({ + typeElement, + sourceCode, +}: { + typeElement: TSESTree.TypeElement + sourceCode: TSESLint.SourceCode +}): string => { + let name: string + + let formatName = (value: string): string => value.replace(/[,;]$/u, '') + + if (typeElement.type === 'TSPropertySignature') { + if (typeElement.key.type === 'Identifier') { + ;({ name } = typeElement.key) + } else if (typeElement.key.type === 'Literal') { + name = `${typeElement.key.value}` + } else { + let end: number = + typeElement.typeAnnotation?.range.at(0) ?? + typeElement.range.at(1)! - (typeElement.optional ? '?'.length : 0) + name = sourceCode.text.slice(typeElement.range.at(0), end) + } + } else if (typeElement.type === 'TSIndexSignature') { + let endIndex: number = + typeElement.typeAnnotation?.range.at(0) ?? typeElement.range.at(1)! + + name = formatName(sourceCode.text.slice(typeElement.range.at(0), endIndex)) + } else if ( + typeElement.type === 'TSMethodSignature' && + 'name' in typeElement.key + ) { + ;({ name } = typeElement.key) + /* v8 ignore next 8 - Unsure if we can reach it */ + } else { + name = formatName( + sourceCode.text.slice(typeElement.range.at(0), typeElement.range.at(1)), + ) + } + return name +} diff --git a/rules/sort-object-types.types.ts b/rules/sort-object-types.types.ts index 5f26f6023..ed61ad027 100644 --- a/rules/sort-object-types.types.ts +++ b/rules/sort-object-types.types.ts @@ -8,26 +8,31 @@ import { elementNamePatternJsonSchema, } from '../utils/common-json-schemas' -export type Options = [ - Partial<{ - customGroups: Record | CustomGroup[] - type: 'alphabetical' | 'line-length' | 'natural' | 'custom' - /** - * @deprecated for {@link `groups`} - */ - groupKind: 'required-first' | 'optional-first' | 'mixed' - partitionByComment: string[] | boolean | string - newlinesBetween: 'ignore' | 'always' | 'never' - specialCharacters: 'remove' | 'trim' | 'keep' - locales: NonNullable - groups: (Group[] | Group)[] - partitionByNewLine: boolean - ignorePattern: string[] - order: 'desc' | 'asc' - ignoreCase: boolean - alphabet: string - }>, -] +export type Options = Partial<{ + useConfigurationIf: { + declarationMatchesPattern?: string + allNamesMatchPattern?: string + } + type: 'alphabetical' | 'line-length' | 'unsorted' | 'natural' | 'custom' + customGroups: Record | CustomGroup[] + /** + * @deprecated for {@link `groups`} + */ + groupKind: 'required-first' | 'optional-first' | 'mixed' + partitionByComment: string[] | boolean | string + newlinesBetween: 'ignore' | 'always' | 'never' + specialCharacters: 'remove' | 'trim' | 'keep' + locales: NonNullable + groups: (Group[] | Group)[] + partitionByNewLine: boolean + /** + * @deprecated for {@link `useConfigurationIf.declarationMatchesPattern`} + */ + ignorePattern: string[] + order: 'desc' | 'asc' + ignoreCase: boolean + alphabet: string +}>[] export type SingleCustomGroup = ( | BaseSingleCustomGroup diff --git a/test/sort-interfaces.test.ts b/test/sort-interfaces.test.ts index 6acd0ef0d..34f82a5bb 100644 --- a/test/sort-interfaces.test.ts +++ b/test/sort-interfaces.test.ts @@ -2038,6 +2038,137 @@ describe(ruleName, () => { valid: [], }, ) + + describe(`${ruleName}(${type}): allows to use 'useConfigurationIf'`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use 'allNamesMatchPattern'`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'g', + leftGroup: 'b', + right: 'g', + left: 'b', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + { + data: { + rightGroup: 'r', + leftGroup: 'g', + right: 'r', + left: 'g', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + options: [ + { + ...options, + useConfigurationIf: { + allNamesMatchPattern: 'foo', + }, + }, + { + ...options, + customGroups: { + r: 'r', + g: 'g', + b: 'b', + }, + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + groups: ['r', 'g', 'b'], + }, + ], + output: dedent` + interface Interface { + r: string + g: string + b: string + } + `, + code: dedent` + interface Interface { + b: string + g: string + r: string + } + `, + }, + ], + valid: [], + }, + ) + + describe(`${ruleName}(${type}): allows to use 'declarationMatchesPattern'`, () => { + ruleTester.run( + `${ruleName}(${type}): detects declaration name by pattern`, + rule, + { + invalid: [ + { + options: [ + { + useConfigurationIf: { + declarationMatchesPattern: '^Interface$', + }, + type: 'unsorted', + }, + options, + ], + errors: [ + { + data: { + right: 'a', + left: 'b', + }, + messageId: 'unexpectedInterfacePropertiesOrder', + }, + ], + output: dedent` + interface OtherInterface { + a: string + b: string + } + `, + code: dedent` + interface OtherInterface { + b: string + a: string + } + `, + }, + ], + valid: [ + { + options: [ + { + useConfigurationIf: { + declarationMatchesPattern: '^Interface$', + }, + type: 'unsorted', + }, + options, + ], + code: dedent` + interface Interface { + b: string + c: string + a: string + } + `, + }, + ], + }, + ) + }) + }) }) describe(`${ruleName}: sorting by natural order`, () => { @@ -3899,6 +4030,26 @@ describe(ruleName, () => { }) describe(`${ruleName}: misc`, () => { + ruleTester.run(`${ruleName}: allows to use "unsorted" as type`, rule, { + valid: [ + { + code: dedent` + interface Interface { + b: string; + c: string; + a: string; + } + `, + options: [ + { + type: 'unsorted', + }, + ], + }, + ], + invalid: [], + }) + ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, rule, diff --git a/test/sort-object-types.test.ts b/test/sort-object-types.test.ts index 52f347e84..3f9ded11a 100644 --- a/test/sort-object-types.test.ts +++ b/test/sort-object-types.test.ts @@ -1885,6 +1885,183 @@ describe(ruleName, () => { invalid: [], }, ) + + describe(`${ruleName}(${type}): allows to use 'useConfigurationIf'`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use 'allNamesMatchPattern'`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'g', + leftGroup: 'b', + right: 'g', + left: 'b', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + { + data: { + rightGroup: 'r', + leftGroup: 'g', + right: 'r', + left: 'g', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + options: [ + { + ...options, + useConfigurationIf: { + allNamesMatchPattern: 'foo', + }, + }, + { + ...options, + customGroups: { + r: 'r', + g: 'g', + b: 'b', + }, + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + groups: ['r', 'g', 'b'], + }, + ], + output: dedent` + type Type = { + r: string + g: string + b: string + } + `, + code: dedent` + type Type = { + b: string + g: string + r: string + } + `, + }, + ], + valid: [], + }, + ) + + describe(`${ruleName}(${type}): allows to use 'declarationMatchesPattern'`, () => { + ruleTester.run( + `${ruleName}(${type}): detects declaration name by pattern`, + rule, + { + invalid: [ + { + options: [ + { + useConfigurationIf: { + declarationMatchesPattern: '^Type$', + }, + type: 'unsorted', + }, + options, + ], + errors: [ + { + data: { + right: 'a', + left: 'b', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + ], + output: dedent` + type OtherType = { + a: string + b: string + } + `, + code: dedent` + type OtherType = { + b: string + a: string + } + `, + }, + ], + valid: [ + { + options: [ + { + useConfigurationIf: { + declarationMatchesPattern: '^Type$', + }, + type: 'unsorted', + }, + options, + ], + code: dedent` + type Type = { + b: string + c: string + a: string + } + `, + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): does not match configuration if no declaration name`, + rule, + { + invalid: [ + { + options: [ + { + useConfigurationIf: { + declarationMatchesPattern: '^Type$', + }, + type: 'unsorted', + }, + options, + ], + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + ], + output: dedent` + type Type = { + a: { + b: string + c: string + } + } + `, + code: dedent` + type Type = { + a: { + c: string + b: string + } + } + `, + }, + ], + valid: [], + }, + ) + }) + }) }) describe(`${ruleName}: sorting by natural order`, () => { @@ -3133,6 +3310,26 @@ describe(ruleName, () => { }) describe('misc', () => { + ruleTester.run(`${ruleName}: allows to use "unsorted" as type`, rule, { + valid: [ + { + code: dedent` + type Type = { + b: string; + c: string; + a: string; + } + `, + options: [ + { + type: 'unsorted', + }, + ], + }, + ], + invalid: [], + }) + ruleTester.run(`${ruleName}: ignores semi at the end of value`, rule, { valid: [ dedent`