Skip to content

Commit

Permalink
Improve typing
Browse files Browse the repository at this point in the history
  • Loading branch information
stormwarning committed Jan 18, 2024
1 parent bd033c9 commit 28dcd29
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 124 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { orderRule } from './lib/rules/order'
import { orderRule } from './lib/rules/order.js'

const plugin = {
rules: {
Expand Down
84 changes: 51 additions & 33 deletions src/lib/rules/order.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AST_NODE_TYPES, ESLintUtils, type TSESLint, type TSESTree } from '@typescript-eslint/utils'
import type { Rule } from 'eslint'
import type { ImportDeclaration } from 'estree'

import { mutateRanksToAlphabetize } from '../utils/alphabetize-ranks'
import { makeNewlinesBetweenReport } from '../utils/make-newlines-between-report'
import { makeOutOfOrderReport } from '../utils/make-out-of-order-report'
import { resolveImportGroup } from '../utils/resolve-import-group'
import { mutateRanksToAlphabetize } from '../utils/alphabetize-ranks.js'
import { makeNewlinesBetweenReport } from '../utils/make-newlines-between-report.js'
import { makeOutOfOrderReport } from '../utils/make-out-of-order-report.js'
import { resolveImportGroup } from '../utils/resolve-import-group.js'

const IMPORT_GROUPS = [
'builtin',
Expand All @@ -19,15 +20,17 @@ const IMPORT_GROUPS = [
type ImportGroup = (typeof IMPORT_GROUPS)[number]
type GroupRankMap = Record<(typeof IMPORT_GROUPS)[number], number>

export type ImportNode = ImportDeclaration & Rule.NodeParentExtension
export type ImportNode = (TSESTree.ImportDeclaration | TSESTree.TSImportEqualsDeclaration) & {
parent: TSESTree.Node
}

type ImportName = ImportDeclaration['source']['value']

export interface ImportNodeObject {
node: Rule.Node & { importKind?: string }
node: ImportNode // & { importKind?: string }
value: ImportName
displayName: ImportName
type: 'import' | 'import:'
type: 'import' | 'import:object'
rank: number
}

Expand All @@ -37,11 +40,11 @@ interface RankObject {
}

function computeRank(
context: Rule.RuleContext,
importEntry: Omit<ImportNodeObject, 'rank'>,
settings: TSESLint.SharedConfigurationSettings,
importEntry: ImportNodeObject,
ranks: RankObject,
) {
let kind: ImportGroup = resolveImportGroup(importEntry.value as string, context)
let kind: ImportGroup = resolveImportGroup(importEntry.value as string, settings)
let rank: number

/**
Expand All @@ -65,14 +68,14 @@ function computeRank(
}

function registerNode(
context: Rule.RuleContext,
importEntry: Omit<ImportNodeObject, 'rank'>,
settings: TSESLint.SharedConfigurationSettings,
importEntry: ImportNodeObject,
ranks: RankObject,
imported,
imported?: unknown[],
) {
let rank = computeRank(context, importEntry, ranks)
let rank = computeRank(settings, importEntry, ranks)
if (rank !== -1) {
imported.push({ ...importEntry, rank })
imported?.push({ ...importEntry, rank })
}
}

Expand Down Expand Up @@ -109,29 +112,38 @@ function convertGroupsToRanks(groups: typeof IMPORT_GROUPS) {
return { groups: ranks, omittedTypes }
}

export const orderRule: Rule.RuleModule = {
// eslint-disable-next-line new-cap
const createRule = ESLintUtils.RuleCreator(
(name) =>
`https://github.com/stormwarning/eslint-plugin-import-sorting/blob/main/docs/rules/${name}.md`,
)

export const orderRule = createRule({
name: 'order',
meta: {
type: 'layout',
fixable: 'code',
docs: {
description: 'Enforce a convention in the order of `import` statements.',
url: 'https://github.com/stormwarning/eslint-plugin-import-sorting/blob/main/docs/rules/order.md',
},
schema: [
{
type: 'object',
},
],
messages: {
'needs-newline': 'There should be at least one empty line between import groups',
'extra-newline': 'There should be no empty line between import groups',
'extra-newline-in-group': 'There should be no empty line within import group',
'out-of-order': '{{secondImport}} should occur {{order}} {{firstImport}}',
},
schema: [],
},
defaultOptions: [],
create(context) {
let importMap: Map<Rule.Node, Array<ImportNodeObject>> = new Map()
let importMap = new Map<ImportNode, ImportNodeObject[]>()
let { groups, omittedTypes } = convertGroupsToRanks(IMPORT_GROUPS)
let ranks: RankObject = {
groups,
omittedTypes,
}

function getBlockImports(node: Rule.Node) {
function getBlockImports(node: ImportNode) {
if (!importMap.has(node)) {
importMap.set(node, [])
}
Expand All @@ -145,32 +157,36 @@ export const orderRule: Rule.RuleModule = {
if (node.specifiers.length > 0) {
let name = node.source.value
registerNode(
context,
context.settings,
{
node,
value: name,
displayName: name,
type: 'import',
/** @todo Check that setting this to a value doesn't cause problems. */
rank: 0,
},
ranks,
getBlockImports(node.parent),
)
}
},
TSImportEqualsDeclaration(node: ImportNode) {
TSImportEqualsDeclaration(node) {
// @ts-expect-error -- Probably don't need this check.
if (node.isExport) return

let displayName
let value
let type
let displayName: string
let value: string
let type: 'import' | 'import:object'

if (node.moduleReference.type === 'TSExternalModuleReference') {
value = node.moduleReference.expression.value
if (node.moduleReference.type === AST_NODE_TYPES.TSExternalModuleReference) {
/** @todo Not sure how to properly type this property. */
value = node.moduleReference.expression.value as string
displayName = value
type = 'import'
} else {
value = ''
displayName = context.getSourceCode().getText(node.moduleReference)
displayName = context.sourceCode.getText(node.moduleReference)
type = 'import:object'
}

Expand All @@ -181,6 +197,8 @@ export const orderRule: Rule.RuleModule = {
value,
displayName,
type,
/** @todo Check that setting this to a value doesn't cause problems. */
rank: 0,
},
ranks,
getBlockImports(node.parent),
Expand All @@ -199,4 +217,4 @@ export const orderRule: Rule.RuleModule = {
},
}
},
}
})
17 changes: 10 additions & 7 deletions src/lib/utils/alphabetize-ranks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'node:path'

import groupBy from 'object.groupby'

import type { ImportNodeObject } from '../rules/order'
import type { ImportNodeObject } from '../rules/order.js'

type OrderDirection = 'asc' | 'desc'

Expand All @@ -21,10 +21,10 @@ function compareString(first: string, second: string) {
function compareDotSegments(first: string, second: string) {
let regex = /\.+(?=\/)/g

let firstCount = (first.match(regex) || []).join('').length
let secondCount = (second.match(regex) || []).join('').length
let firstCount = (first.match(regex) ?? []).join('').length
let secondCount = (second.match(regex) ?? []).join('').length

if (firstCount > secondCount) return -1
if (secondCount < firstCount) return -1
if (firstCount < secondCount) return 1

// If segment length is the same, compare the basename alphabetically.
Expand Down Expand Up @@ -69,16 +69,19 @@ function getSorter(order: OrderDirection) {
result =
multiplierImportKind *
compareString(
nodeA.node.importKind || DEFAULT_IMPORT_KIND,
nodeB.node.importKind || DEFAULT_IMPORT_KIND,
nodeA.node.importKind ?? DEFAULT_IMPORT_KIND,
nodeB.node.importKind ?? DEFAULT_IMPORT_KIND,
)
}

return result
}
}

export function mutateRanksToAlphabetize(imported: ImportNodeObject[], alphabetizeOptions) {
export function mutateRanksToAlphabetize(
imported: ImportNodeObject[],
alphabetizeOptions: OrderDirection,
) {
let groupedByRanks: Record<number, ImportNodeObject[]> = groupBy(
imported,
(item: ImportNodeObject) => item.rank,
Expand Down
18 changes: 9 additions & 9 deletions src/lib/utils/find-comment.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Rule, SourceCode } from 'eslint'
import type { TSESLint } from '@typescript-eslint/utils'

import { takeTokensAfterWhile, takeTokensBeforeWhile } from './take-tokens'
import type { ImportNode } from '../rules/order.js'
import { takeTokensAfterWhile, takeTokensBeforeWhile } from './take-tokens.js'

export function findStartOfLineWithComments(sourceCode: SourceCode, node: Rule.Node) {
export function findStartOfLineWithComments(sourceCode: TSESLint.SourceCode, node: ImportNode) {
let tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node))
let startOfTokens = (
let startOfTokens =
tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range?.[0] : node.range?.[0]
) as number
let result = startOfTokens

for (let index = startOfTokens - 1; index > 0; index--) {
Expand All @@ -20,11 +20,11 @@ export function findStartOfLineWithComments(sourceCode: SourceCode, node: Rule.N
return result
}

export function findEndOfLineWithComments(sourceCode: SourceCode, node: Rule.Node) {
export function findEndOfLineWithComments(sourceCode: TSESLint.SourceCode, node: ImportNode) {
let tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node))
let endOfTokens = (
tokensToEndOfLine.length > 0 ? tokensToEndOfLine.at(-1)?.range?.[1] : node.range?.[1]
) as number
)!
let result = endOfTokens

for (let index = endOfTokens; index < sourceCode.text.length; index++) {
Expand All @@ -48,9 +48,9 @@ export function findEndOfLineWithComments(sourceCode: SourceCode, node: Rule.Nod
}

/** @todo rename to has-comment-on-same-line */
export function commentOnSameLineAs(node: Rule.Node) {
export function commentOnSameLineAs(node: ImportNode) {
return (token: any) =>
(token.type === 'Block' || token.type === 'Line') &&
token.loc.start.line === token.loc.end.line &&
token.loc.end.line === node.loc?.end.line
token.loc.end.line === node.loc.end.line
}
7 changes: 4 additions & 3 deletions src/lib/utils/find-root-node.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Rule } from 'eslint'
import type { ImportNode } from '../rules/order.js'

export function findRootNode(node: Rule.Node) {
export function findRootNode(node: ImportNode) {
let parent = node

/** @todo Not sure how to properly type `parent` property. */
// eslint-disable-next-line no-eq-null, eqeqeq
while (parent.parent != null && parent.parent.body == null) {
parent = parent.parent
parent = parent.parent as ImportNode
}

return parent
Expand Down
38 changes: 22 additions & 16 deletions src/lib/utils/make-newlines-between-report.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import type { AST, Rule } from 'eslint'
/* eslint-disable @typescript-eslint/ban-types */

import type { ImportNodeObject } from '../rules/order'
import type { TSESLint } from '@typescript-eslint/utils'
import type { AST } from 'eslint'

import type { ImportNodeObject } from '../rules/order.js'
import {
commentOnSameLineAs,
findEndOfLineWithComments,
findStartOfLineWithComments,
} from './find-comment'
import { findRootNode } from './find-root-node'
import { takeTokensAfterWhile } from './take-tokens'
} from './find-comment.js'
import { findRootNode } from './find-root-node.js'
import { takeTokensAfterWhile } from './take-tokens.js'

function fixNewLineAfterImport(context: Rule.RuleContext, previousImport: ImportNodeObject) {
function fixNewLineAfterImport(
context: TSESLint.RuleContext<string, []>,
previousImport: ImportNodeObject,
) {
let previousRoot = findRootNode(previousImport.node)
let tokensToEndOfLine = takeTokensAfterWhile(
context.sourceCode,
previousRoot,
commentOnSameLineAs(previousRoot),
)

let endOfLine = previousRoot.range?.[1] as number
let endOfLine = previousRoot.range[1]
if (tokensToEndOfLine.length > 0) {
endOfLine = tokensToEndOfLine.at(-1)?.range?.[1] as number
endOfLine = tokensToEndOfLine.at(-1)!.range[1]
}

return (fixer: Rule.RuleFixer) =>
fixer.insertTextAfterRange([previousRoot.range?.[0] as number, endOfLine], '\n')
return (fixer: TSESLint.RuleFixer) =>
fixer.insertTextAfterRange([previousRoot.range[0], endOfLine], '\n')
}

function removeNewLineAfterImport(
context: Rule.RuleContext,
context: TSESLint.RuleContext<string, []>,
currentImport: ImportNodeObject,
previousImport: ImportNodeObject,
) {
Expand All @@ -40,14 +46,14 @@ function removeNewLineAfterImport(
]

if (/^\s*$/.test(sourceCode.text.slice(rangeToRemove[0], rangeToRemove[1]))) {
return (fixer: Rule.RuleFixer) => fixer.removeRange(rangeToRemove)
return (fixer: TSESLint.RuleFixer) => fixer.removeRange(rangeToRemove)
}

return undefined
}

export function makeNewlinesBetweenReport(
context: Rule.RuleContext,
context: TSESLint.RuleContext<string, []>,
imported: ImportNodeObject[],
newlinesBetweenImports = 'always',
) {
Expand Down Expand Up @@ -79,7 +85,7 @@ export function makeNewlinesBetweenReport(
if (isStartOfDistinctGroup) {
context.report({
node: previousImport.node,
message: 'There should be at least one empty line between import groups',
messageId: 'needs-newline',
fix: fixNewLineAfterImport(context, previousImport),
})
}
Expand All @@ -89,14 +95,14 @@ export function makeNewlinesBetweenReport(
) {
context.report({
node: previousImport.node,
message: 'There should be no empty line within import group',
messageId: 'extra-newline',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
})
}
} else if (emptyLinesBetween > 0) {
context.report({
node: previousImport.node,
message: 'There should be no empty line between import groups',
messageId: 'extra-newline-in-group',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
})
}
Expand Down
Loading

0 comments on commit 28dcd29

Please sign in to comment.