From 4b81340966730c6847c46ed9bc26b01b48f5c8fd Mon Sep 17 00:00:00 2001
From: tthornton3-chwy <83608949+tthornton3-chwy@users.noreply.github.com>
Date: Sat, 11 Nov 2023 12:04:19 -0500
Subject: [PATCH] feat: add max line length option for multiline imports
sorting
---
.gitignore | 1 +
docs/rules/sort-imports.md | 9 +++
package.json | 3 +-
pnpm-lock.yaml | 3 +
rules/sort-imports.ts | 41 +++++++++++
test/sort-imports.test.ts | 122 +++++++++++++++++++++++++++++++-
test/utils/are-options-valid.ts | 60 ++++++++++++++++
typings/index.ts | 1 +
utils/compare.ts | 23 +++++-
9 files changed, 260 insertions(+), 3 deletions(-)
create mode 100644 test/utils/are-options-valid.ts
diff --git a/.gitignore b/.gitignore
index fa65a35a8..6125d01eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Node.js
node_modules/
+.nvmrc
# Build
dist/
diff --git a/docs/rules/sort-imports.md b/docs/rules/sort-imports.md
index c9ea7709f..b14f98475 100644
--- a/docs/rules/sort-imports.md
+++ b/docs/rules/sort-imports.md
@@ -127,6 +127,7 @@ interface Options {
}
'internal-pattern'?: string[]
'newlines-between'?: 'always' | 'ignore' | 'never'
+ 'max-line-length'?: number
```
### type
@@ -244,6 +245,14 @@ The [minimatch](https://github.com/isaacs/minimatch) library is used for pattern
- `always` - one new line between each group will be enforced, and new lines inside a group will be forbidden.
- `never` - no new lines are allowed in the entire import section.
+### max-line-length\*
+
+(default: `undefined`)
+
+You can use this to sort by the import name only, excluding the elements, when the line length is greater than this number.
+
+**\*Note:** Only available for `line-length` type.
+
## ⚙️ Usage
::: code-group
diff --git a/package.json b/package.json
index 30038c632..7893f0e02 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
"@typescript-eslint/rule-tester": "^6.9.1",
"@typescript-eslint/types": "^6.9.1",
"@vitest/coverage-v8": "^0.34.6",
+ "ajv": "^6.12.6",
"astro-eslint-parser": "^0.16.0",
"changelogen": "^0.5.5",
"clean-publish": "^4.2.0",
@@ -125,4 +126,4 @@
"vitest": "^0.34.6",
"vue": "^3.3.7"
}
-}
\ No newline at end of file
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dd1b040b9..89d27eaa2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -52,6 +52,9 @@ devDependencies:
'@vitest/coverage-v8':
specifier: ^0.34.6
version: 0.34.6(vitest@0.34.6)
+ ajv:
+ specifier: ^6.12.6
+ version: 6.12.6
astro-eslint-parser:
specifier: ^0.16.0
version: 0.16.0
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index 292b1fe9e..41d44f26a 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -59,6 +59,7 @@ type Options = [
'newlines-between': NewlinesBetweenValue
groups: (Group[] | Group)[]
'internal-pattern': string[]
+ 'max-line-length'?: number
'ignore-case': boolean
order: SortOrder
type: SortType
@@ -81,6 +82,7 @@ export default createEslintRule, MESSAGE_ID>({
fixable: 'code',
schema: [
{
+ id: 'sort-imports',
type: 'object',
properties: {
'custom-groups': {
@@ -132,8 +134,37 @@ export default createEslintRule, MESSAGE_ID>({
default: NewlinesBetweenValue.always,
type: 'string',
},
+ 'max-line-length': {
+ type: 'integer',
+ minimum: 0,
+ exclusiveMinimum: true,
+ },
},
+ allOf: [
+ { $ref: '#/definitions/max-line-length-requires-line-length-type' },
+ ],
additionalProperties: false,
+ dependencies: {
+ 'max-line-length': ['type'],
+ },
+ definitions: {
+ 'is-line-length': {
+ properties: {
+ type: { enum: [SortType['line-length']], type: 'string' },
+ },
+ required: ['type'],
+ type: 'object',
+ },
+ 'max-line-length-requires-line-length-type': {
+ anyOf: [
+ {
+ not: { required: ['max-line-length'], type: 'object' },
+ type: 'object',
+ },
+ { $ref: '#/definitions/is-line-length' },
+ ],
+ },
+ },
},
],
messages: {
@@ -312,6 +343,10 @@ export default createEslintRule, MESSAGE_ID>({
return getGroup()
}
+ let hasMultipleImportDeclarations = (
+ node: TSESTree.ImportDeclaration,
+ ): boolean => node.specifiers.length > 1
+
let registerNode = (node: ModuleDeclaration) => {
let name: string
@@ -333,6 +368,12 @@ export default createEslintRule, MESSAGE_ID>({
group: computeGroup(node),
name,
node,
+ ...(options.type === SortType['line-length'] &&
+ options['max-line-length'] && {
+ hasMultipleImportDeclarations: hasMultipleImportDeclarations(
+ node as TSESTree.ImportDeclaration,
+ ),
+ }),
})
}
diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts
index 8286b09d6..98bb2fa27 100644
--- a/test/sort-imports.test.ts
+++ b/test/sort-imports.test.ts
@@ -1,8 +1,9 @@
import { RuleTester } from '@typescript-eslint/rule-tester'
-import { afterAll, describe, it } from 'vitest'
+import { afterAll, describe, expect, it } from 'vitest'
import { dedent } from 'ts-dedent'
import rule, { NewlinesBetweenValue, RULE_NAME } from '../rules/sort-imports'
+import { areOptionsValid } from './utils/are-options-valid'
import { SortOrder, SortType } from '../typings'
describe(RULE_NAME, () => {
@@ -3474,6 +3475,125 @@ describe(RULE_NAME, () => {
],
},
)
+
+ describe(`${RULE_NAME}(${type}): support max line length`, () => {
+ ruleTester.run('rule', rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import { ThisIsApprox, SeventyNine } from '~CharactersLongAndShouldNotBeSplit';
+ import { EvenThoughThisIsLongItShouldNotGetSplitUpAsItThereIsOnlyOne } from 'IWillNotBeSplitUp';
+ import Short from 'app/components/LongName';
+ import {
+ ICantBelieveHowLong,
+ ICantHandleHowLong,
+ KindaLong,
+ Long,
+ ThisIsTheLongestEver,
+ WowSoLong,
+ } from 'app/components/Short';
+ import EvenThoughThisIsLongItShouldNotBePutOntoAnyNewLinesAsThereIsOnlyOne from 'IWillNotBePutOntoNewLines';
+ import ThereIsTwoOfMe, {
+ SoWeShouldSplitUpSinceWeAreInDifferentSections
+ } from 'IWillDefinitelyBeSplitUp';
+ `,
+ output: dedent`
+ import {
+ ICantBelieveHowLong,
+ ICantHandleHowLong,
+ KindaLong,
+ Long,
+ ThisIsTheLongestEver,
+ WowSoLong,
+ } from 'app/components/Short';
+ import ThereIsTwoOfMe, {
+ SoWeShouldSplitUpSinceWeAreInDifferentSections
+ } from 'IWillDefinitelyBeSplitUp';
+ import Short from 'app/components/LongName';
+ import { ThisIsApprox, SeventyNine } from '~CharactersLongAndShouldNotBeSplit';
+ import { EvenThoughThisIsLongItShouldNotGetSplitUpAsItThereIsOnlyOne } from 'IWillNotBeSplitUp';
+ import EvenThoughThisIsLongItShouldNotBePutOntoAnyNewLinesAsThereIsOnlyOne from 'IWillNotBePutOntoNewLines';
+ `,
+ options: [
+ {
+ ...options,
+ order: SortOrder.asc,
+ 'max-line-length': 80,
+ groups: [
+ 'type',
+ ['builtin', 'external'],
+ 'internal-type',
+ 'internal',
+ ['parent-type', 'sibling-type', 'index-type'],
+ ['parent', 'sibling', 'index'],
+ 'object',
+ 'unknown',
+ ],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: 'IWillNotBeSplitUp',
+ right: 'app/components/LongName',
+ },
+ },
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: 'app/components/LongName',
+ right: 'app/components/Short',
+ },
+ },
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: 'IWillNotBePutOntoNewLines',
+ right: 'IWillDefinitelyBeSplitUp',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ let subType = 'schema'
+
+ it(`${subType} -- type must be set if 'max-line-length' is`, () => {
+ expect(
+ areOptionsValid(rule, {
+ ...options,
+ type: undefined,
+ 'max-line-length': 80,
+ }),
+ ).toBe(
+ 'data[0] should have property type when property max-line-length is present',
+ )
+ })
+
+ it(`${subType} -- type must be set to 'line-length' if 'max-line-length' is set`, () => {
+ expect(
+ areOptionsValid(rule, {
+ ...options,
+ type: SortType.alphabetical,
+ 'max-line-length': 80,
+ }),
+ ).toBe(
+ 'data[0] should NOT be valid, data[0].type should be equal to one of the allowed values, data[0] should match some schema in anyOf',
+ )
+ })
+
+ it(`${subType} -- if it's set, 'max-line-length' must be greater than 0`, () => {
+ expect(
+ areOptionsValid(rule, {
+ ...options,
+ 'max-line-length': 0,
+ }),
+ ).toBe("data[0]['max-line-length'] should be > 0")
+ })
+ })
})
describe(`${RULE_NAME}: misc`, () => {
diff --git a/test/utils/are-options-valid.ts b/test/utils/are-options-valid.ts
new file mode 100644
index 000000000..d75daff4a
--- /dev/null
+++ b/test/utils/are-options-valid.ts
@@ -0,0 +1,60 @@
+import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
+import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'
+
+import * as jsonSpecv4 from 'ajv/lib/refs/json-schema-draft-04.json'
+import { TSUtils } from '@typescript-eslint/utils'
+import Ajv from 'ajv'
+
+// Lovingly borrowed from: https://github.com/typescript-eslint/typescript-eslint/pull/6947/files
+// TODO: It would be nicer to have https://github.com/eslint/eslint/pull/16823, but it's back in draft.
+const ajv = new Ajv({
+ schemaId: 'auto',
+ async: false,
+ meta: false,
+})
+ajv.addMetaSchema(jsonSpecv4)
+// @ts-ignore
+ajv._opts.defaultMeta = jsonSpecv4.id
+// @ts-ignore
+ajv._refs['http://json-schema.org/schema'] =
+ 'http://json-schema.org/draft-04/schema'
+
+export let areOptionsValid = (
+ rule: RuleModule,
+ options: unknown,
+): boolean | string => {
+ let normalizedSchema = normalizeSchema(rule.meta.schema)
+
+ let validate = ajv.compile(normalizedSchema)
+
+ let valid = validate([options])
+ if (typeof valid !== 'boolean') {
+ // Schema could not validate options synchronously. This is not allowed for ESLint rules.
+ return false
+ }
+
+ return valid || ajv.errorsText(validate.errors)
+}
+
+let normalizeSchema = (
+ schema: readonly JSONSchema4[] | JSONSchema4,
+): JSONSchema4 => {
+ if (!TSUtils.isArray(schema)) {
+ return schema
+ }
+
+ if (schema.length === 0) {
+ return {
+ type: 'array',
+ minItems: 0,
+ maxItems: 0,
+ }
+ }
+
+ return {
+ items: schema as JSONSchema4[],
+ maxItems: schema.length,
+ type: 'array',
+ minItems: 0,
+ }
+}
diff --git a/typings/index.ts b/typings/index.ts
index aea0eb6fc..ac2ae42c9 100644
--- a/typings/index.ts
+++ b/typings/index.ts
@@ -14,6 +14,7 @@ export enum SortOrder {
export type PartitionComment = string[] | boolean | string
export interface SortingNode {
+ hasMultipleImportDeclarations?: boolean
dependencies?: string[]
node: TSESTree.Node
group?: string
diff --git a/utils/compare.ts b/utils/compare.ts
index 01dd8af9c..49fa0e97d 100644
--- a/utils/compare.ts
+++ b/utils/compare.ts
@@ -8,6 +8,7 @@ export let compare = (
a: SortingNode,
b: SortingNode,
options: {
+ 'max-line-length'?: number
'ignore-case'?: boolean
order: SortOrder
type: SortType
@@ -32,7 +33,27 @@ export let compare = (
sortingFunction = (aNode, bNode) =>
naturalCompare(formatString(aNode.name), formatString(bNode.name))
} else {
- sortingFunction = (aNode, bNode) => aNode.size - bNode.size
+ sortingFunction = (aNode, bNode) => {
+ let aSize = aNode.size
+ let bSize = bNode.size
+
+ let maxLineLength = options['max-line-length']
+
+ if (maxLineLength) {
+ let isTooLong = (size: number, node: SortingNode) =>
+ size > maxLineLength! && node.hasMultipleImportDeclarations
+
+ if (isTooLong(aSize, aNode)) {
+ aSize = aNode.name.length + 10
+ }
+
+ if (isTooLong(bSize, bNode)) {
+ bSize = bNode.name.length + 10
+ }
+ }
+
+ return aSize - bSize
+ }
}
return orderCoefficient * sortingFunction(a, b)