Skip to content

Commit

Permalink
feat(sort-imports): max line length
Browse files Browse the repository at this point in the history
  • Loading branch information
tthornton3-chwy committed Oct 27, 2023
1 parent e53171f commit 25c54b7
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Node.js
node_modules/
.nvmrc

# Build
dist/
Expand All @@ -11,6 +12,7 @@ dist/
# VitePress
docs/.vitepress/cache/
docs/.vitepress/temp/
.vitepress/

# Test
coverage/
Expand Down
9 changes: 9 additions & 0 deletions docs/rules/sort-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ interface Options {
}
'internal-pattern'?: string[]
'newlines-between'?: 'always' | 'ignore' | 'never'
'max-line-length'?: number
```
### type
Expand Down Expand Up @@ -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\*
<sub>(default: `undefined`)</sub>
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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"@typescript-eslint/rule-tester": "^6.9.0",
"@typescript-eslint/types": "^6.9.0",
"@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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 47 additions & 1 deletion rules/sort-imports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TSESTree } from '@typescript-eslint/types'
import type { TSESLint } from '@typescript-eslint/utils'
import type { TSESTree } from '@typescript-eslint/types'

import { AST_NODE_TYPES } from '@typescript-eslint/types'
import { builtinModules } from 'node:module'
import { minimatch } from 'minimatch'

Expand Down Expand Up @@ -59,6 +60,7 @@ type Options<T extends string[]> = [
'newlines-between': NewlinesBetweenValue
groups: (Group<T>[] | Group<T>)[]
'internal-pattern': string[]
'max-line-length'?: number
'ignore-case': boolean
order: SortOrder
type: SortType
Expand All @@ -81,6 +83,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
fixable: 'code',
schema: [
{
id: 'sort-imports',
type: 'object',
properties: {
'custom-groups': {
Expand Down Expand Up @@ -132,8 +135,37 @@ export default createEslintRule<Options<string[]>, 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: {
Expand Down Expand Up @@ -312,6 +344,14 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
return getGroup()
}

let hasMultipleImportDeclarations = (
node: TSESTree.ImportDeclaration,
): boolean =>
node.specifiers.length > 1 &&
node.specifiers.filter(
specifier => specifier.type === AST_NODE_TYPES.ImportSpecifier,
).length > 1

let registerNode = (node: ModuleDeclaration) => {
let name: string

Expand All @@ -333,6 +373,12 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
group: computeGroup(node),
name,
node,
...(options.type === SortType['line-length'] &&
options['max-line-length'] && {
hasMultipleImportDeclarations: hasMultipleImportDeclarations(
node as TSESTree.ImportDeclaration,
),
}),
})
}

Expand Down
118 changes: 117 additions & 1 deletion test/sort-imports.test.ts
Original file line number Diff line number Diff line change
@@ -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/areOptionsValid'
import { SortOrder, SortType } from '../typings'

describe(RULE_NAME, () => {
Expand Down Expand Up @@ -3474,6 +3475,121 @@ 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 EvenThoughThereIsTwoOfMe, { WeWontBeSplitUpAsThereIsOnlyOneOfThese } from 'IWillNotBeSplitUp';
`,
output: dedent`
import {
ICantBelieveHowLong,
ICantHandleHowLong,
KindaLong,
Long,
ThisIsTheLongestEver,
WowSoLong,
} from 'app/components/Short';
import Short from 'app/components/LongName';
import { ThisIsApprox, SeventyNine } from '~CharactersLongAndShouldNotBeSplit';
import { EvenThoughThisIsLongItShouldNotGetSplitUpAsItThereIsOnlyOne } from 'IWillNotBeSplitUp';
import EvenThoughThereIsTwoOfMe, { WeWontBeSplitUpAsThereIsOnlyOneOfThese } 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: 'IWillNotBeSplitUp',
},
},
],
},
],
})

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`, () => {
Expand Down
60 changes: 60 additions & 0 deletions test/utils/areOptionsValid.ts
Original file line number Diff line number Diff line change
@@ -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<string, readonly unknown[]>,
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,
}
}
1 change: 1 addition & 0 deletions typings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion utils/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export let compare = (
a: SortingNode,
b: SortingNode,
options: {
'max-line-length'?: number
'ignore-case'?: boolean
order: SortOrder
type: SortType
Expand All @@ -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)
Expand Down

0 comments on commit 25c54b7

Please sign in to comment.