diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts index 1ddc50d9..d639a7b0 100644 --- a/rules/sort-imports.ts +++ b/rules/sort-imports.ts @@ -67,6 +67,7 @@ export const RULE_NAME = 'sort-imports' type ModuleDeclaration = | TSESTree.TSImportEqualsDeclaration + | TSESTree.VariableDeclaration | TSESTree.ImportDeclaration type SortingNodeWithGroup = SortingNode & { @@ -165,6 +166,13 @@ export default createEslintRule, MESSAGE_ID>({ let nodes: SortingNodeWithGroup[] = [] + let getRequireModuleName = (node: TSESTree.VariableDeclaration) => + ( + ( + node.declarations.at(0)!.init! as TSESTree.CallExpression + ).arguments.at(0) as TSESTree.Literal + ).value as string + let computeGroup = (node: ModuleDeclaration): Group => { let group: Group | undefined @@ -194,12 +202,12 @@ export default createEslintRule, MESSAGE_ID>({ } } - let isInternal = (nodeElement: TSESTree.ImportDeclaration) => + let isInternal = (value: string) => (options['internal-pattern'].length && options['internal-pattern'].some(pattern => - minimatch(nodeElement.source.value, pattern), + minimatch(value, pattern), )) || - tsPaths.some(pattern => minimatch(nodeElement.source.value, pattern)) + tsPaths.some(pattern => minimatch(value, pattern)) let determineCustomGroup = ( groupType: 'value' | 'type', @@ -221,7 +229,7 @@ export default createEslintRule, MESSAGE_ID>({ } } - if (node.importKind === 'type') { + if (node.type !== 'VariableDeclaration' && node.importKind === 'type') { if (node.type === AST_NODE_TYPES.ImportDeclaration) { determineCustomGroup('type', node.source.value) @@ -229,7 +237,7 @@ export default createEslintRule, MESSAGE_ID>({ defineGroup('builtin-type') } - if (isInternal(node)) { + if (isInternal(node.source.value)) { defineGroup('internal-type') } @@ -250,34 +258,49 @@ export default createEslintRule, MESSAGE_ID>({ defineGroup('type') } - if (!group && node.type === AST_NODE_TYPES.ImportDeclaration) { - determineCustomGroup('value', node.source.value) + if ( + !group && + (node.type === AST_NODE_TYPES.ImportDeclaration || + node.type === AST_NODE_TYPES.VariableDeclaration) + ) { + let value - if (isCoreModule(node.source.value)) { + if (node.type === AST_NODE_TYPES.ImportDeclaration) { + ;({ value } = node.source) + } else { + value = getRequireModuleName(node) + } + + determineCustomGroup('value', value) + + if (isCoreModule(value)) { defineGroup('builtin') } - if (isInternal(node)) { + if (isInternal(value)) { defineGroup('internal') } - if (isStyle(node.source.value)) { + if (isStyle(value)) { defineGroup('style') } - if (node.specifiers.length === 0) { + if ( + node.type === AST_NODE_TYPES.ImportDeclaration && + node.specifiers.length === 0 + ) { defineGroup('side-effect') } - if (isIndex(node.source.value)) { + if (isIndex(value)) { defineGroup('index') } - if (isParent(node.source.value)) { + if (isParent(value)) { defineGroup('parent') } - if (isSibling(node.source.value)) { + if (isSibling(value)) { defineGroup('sibling') } @@ -292,7 +315,7 @@ export default createEslintRule, MESSAGE_ID>({ if (node.type === AST_NODE_TYPES.ImportDeclaration) { name = node.source.value - } else { + } else if (node.type === AST_NODE_TYPES.TSImportEqualsDeclaration) { if ( node.moduleReference.type === AST_NODE_TYPES.TSExternalModuleReference && @@ -302,6 +325,8 @@ export default createEslintRule, MESSAGE_ID>({ } else { name = source.text.slice(...node.moduleReference.range) } + } else { + name = getRequireModuleName(node) } nodes.push({ @@ -315,6 +340,18 @@ export default createEslintRule, MESSAGE_ID>({ return { TSImportEqualsDeclaration: registerNode, ImportDeclaration: registerNode, + CallExpression: node => { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'require' && + node.arguments.length === 1 && + node.arguments.at(0)!.type === 'Literal' && + node.parent?.type === 'VariableDeclarator' && + node.parent.parent?.type === 'VariableDeclaration' + ) { + registerNode(node.parent.parent) + } + }, 'Program:exit': () => { let getGroupNumber = (node: SortingNodeWithGroup): number => { for (let i = 0, max = options.groups.length; i < max; i++) { diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts index fd5b8ebc..dd1389b4 100644 --- a/test/sort-imports.test.ts +++ b/test/sort-imports.test.ts @@ -1057,6 +1057,80 @@ describe(RULE_NAME, () => { ], }) }) + + it(`${RULE_NAME}(${type}): supports require imports`, () => { + ruleTester.run(RULE_NAME, rule, { + valid: [], + invalid: [ + { + code: dedent` + let { GantaNakami, IsakiMagari } = require('insomniacs') + let TaoUkegawa = require('./school/ukegawa') + let MotokoKanikawa = require('./school/kanikawa') + let KanamiAnamizu = require('./school/anamizu') + let { YuiShiromaru } = require('astronomy-club') + `, + output: dedent` + let { YuiShiromaru } = require('astronomy-club') + let { GantaNakami, IsakiMagari } = require('insomniacs') + + let KanamiAnamizu = require('./school/anamizu') + let MotokoKanikawa = require('./school/kanikawa') + let TaoUkegawa = require('./school/ukegawa') + `, + options: [ + { + type: SortType.alphabetical, + order: SortOrder.asc, + 'ignore-case': true, + 'newlines-between': NewlinesBetweenValue.always, + 'internal-pattern': ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'insomniacs', + right: './school/ukegawa', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/ukegawa', + right: './school/kanikawa', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/kanikawa', + right: './school/anamizu', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/anamizu', + right: 'astronomy-club', + }, + }, + ], + }, + ], + }) + }) }) describe(`${RULE_NAME}: sorting by natural order`, () => { @@ -2106,6 +2180,80 @@ describe(RULE_NAME, () => { ], }) }) + + it(`${RULE_NAME}(${type}): supports require imports`, () => { + ruleTester.run(RULE_NAME, rule, { + valid: [], + invalid: [ + { + code: dedent` + let { GantaNakami, IsakiMagari } = require('insomniacs') + let TaoUkegawa = require('./school/ukegawa') + let MotokoKanikawa = require('./school/kanikawa') + let KanamiAnamizu = require('./school/anamizu') + let { YuiShiromaru } = require('astronomy-club') + `, + output: dedent` + let { YuiShiromaru } = require('astronomy-club') + let { GantaNakami, IsakiMagari } = require('insomniacs') + + let KanamiAnamizu = require('./school/anamizu') + let MotokoKanikawa = require('./school/kanikawa') + let TaoUkegawa = require('./school/ukegawa') + `, + options: [ + { + type: SortType.natural, + order: SortOrder.asc, + 'ignore-case': true, + 'newlines-between': NewlinesBetweenValue.always, + 'internal-pattern': ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'insomniacs', + right: './school/ukegawa', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/ukegawa', + right: './school/kanikawa', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/kanikawa', + right: './school/anamizu', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/anamizu', + right: 'astronomy-club', + }, + }, + ], + }, + ], + }) + }) }) describe(`${RULE_NAME}: sorting by line length`, () => { @@ -3204,6 +3352,73 @@ describe(RULE_NAME, () => { ], }) }) + + it(`${RULE_NAME}(${type}): supports require imports`, () => { + ruleTester.run(RULE_NAME, rule, { + valid: [], + invalid: [ + { + code: dedent` + let { GantaNakami, IsakiMagari } = require('insomniacs') + let TaoUkegawa = require('./school/ukegawa') + let MotokoKanikawa = require('./school/kanikawa') + let KanamiAnamizu = require('./school/anamizu') + let { YuiShiromaru } = require('astronomy-club') + `, + output: dedent` + let { GantaNakami, IsakiMagari } = require('insomniacs') + let { YuiShiromaru } = require('astronomy-club') + + let MotokoKanikawa = require('./school/kanikawa') + let KanamiAnamizu = require('./school/anamizu') + let TaoUkegawa = require('./school/ukegawa') + `, + options: [ + { + type: SortType['line-length'], + order: SortOrder.desc, + 'ignore-case': true, + 'newlines-between': NewlinesBetweenValue.always, + 'internal-pattern': ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'insomniacs', + right: './school/ukegawa', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/ukegawa', + right: './school/kanikawa', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: './school/anamizu', + right: 'astronomy-club', + }, + }, + ], + }, + ], + }) + }) }) describe(`${RULE_NAME}: misc`, () => {