From 4df7fb8e58900e3d94d42dcd963fba8382e1d180 Mon Sep 17 00:00:00 2001 From: "Azat S." Date: Sun, 21 Jul 2024 18:37:49 +0300 Subject: [PATCH] feat: support require in sort-imports --- rules/sort-imports.ts | 74 ++++-- test/sort-imports.test.ts | 514 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 567 insertions(+), 21 deletions(-) diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts index bd1a5ecf0..3a23ccefd 100644 --- a/rules/sort-imports.ts +++ b/rules/sort-imports.ts @@ -63,10 +63,6 @@ type Options = [ }>, ] -type ModuleDeclaration = - | TSESTree.TSImportEqualsDeclaration - | TSESTree.ImportDeclaration - export default createEslintRule, MESSAGE_ID>({ name: 'sort-imports', meta: { @@ -265,7 +261,12 @@ export default createEslintRule, MESSAGE_ID>({ /* Avoid matching on named imports without specifiers */ !/}\s*from\s+/.test(sourceCode.getText(node)) - let computeGroup = (node: ModuleDeclaration): Group => { + let computeGroup = ( + node: + | TSESTree.TSImportEqualsDeclaration + | TSESTree.VariableDeclaration + | TSESTree.ImportDeclaration, + ): Group => { let isStyle = (value: string) => ['.less', '.scss', '.sass', '.styl', '.pcss', '.css', '.sss'].some( extension => value.endsWith(extension), @@ -288,10 +289,10 @@ export default createEslintRule, MESSAGE_ID>({ let { getGroup, defineGroup, setCustomGroups } = useGroups(options.groups) - let isInternal = (nodeElement: TSESTree.ImportDeclaration) => + let isInternal = (value: string) => options.internalPattern.length && options.internalPattern.some(pattern => - minimatch(nodeElement.source.value, pattern, { + minimatch(value, pattern, { nocomment: true, }), ) @@ -319,7 +320,7 @@ export default createEslintRule, MESSAGE_ID>({ let isExternal = (value: string) => !(value.startsWith('.') || value.startsWith('/')) - if (node.importKind === 'type') { + if (node.type !== 'VariableDeclaration' && node.importKind === 'type') { if (node.type === 'ImportDeclaration') { setCustomGroups(options.customGroups.type, node.source.value) @@ -335,7 +336,7 @@ export default createEslintRule, MESSAGE_ID>({ defineGroup('parent-type') } - if (isInternal(node)) { + if (isInternal(node.source.value)) { defineGroup('internal-type') } @@ -351,10 +352,23 @@ export default createEslintRule, MESSAGE_ID>({ defineGroup('type') } - if (node.type === 'ImportDeclaration') { - setCustomGroups(options.customGroups.value, node.source.value) + if ( + node.type === 'ImportDeclaration' || + node.type === 'VariableDeclaration' + ) { + let value = + node.type === 'ImportDeclaration' + ? node.source.value + : ( + (node.declarations[0].init as TSESTree.CallExpression) + .arguments[0] as TSESTree.Literal + ) + .value!.toString() + .toString() + + setCustomGroups(options.customGroups.value, value) - if (isSideEffectImport(node) && isStyle(node.source.value)) { + if (isSideEffectImport(node) && isStyle(value)) { defineGroup('side-effect-style') } @@ -362,31 +376,31 @@ export default createEslintRule, MESSAGE_ID>({ defineGroup('side-effect') } - if (isStyle(node.source.value)) { + if (isStyle(value)) { defineGroup('style') } - if (isIndex(node.source.value)) { + if (isIndex(value)) { defineGroup('index') } - if (isSibling(node.source.value)) { + if (isSibling(value)) { defineGroup('sibling') } - if (isParent(node.source.value)) { + if (isParent(value)) { defineGroup('parent') } - if (isInternal(node)) { + if (isInternal(value)) { defineGroup('internal') } - if (isCoreModule(node.source.value)) { + if (isCoreModule(value)) { defineGroup('builtin') } - if (isExternal(node.source.value)) { + if (isExternal(value)) { defineGroup('external') } } @@ -398,17 +412,25 @@ export default createEslintRule, MESSAGE_ID>({ node: TSESTree.ImportDeclaration, ): boolean => node.specifiers.length > 1 - let registerNode = (node: ModuleDeclaration) => { + let registerNode = ( + node: + | TSESTree.TSImportEqualsDeclaration + | TSESTree.VariableDeclaration + | TSESTree.ImportDeclaration, + ) => { let name: string if (node.type === 'ImportDeclaration') { name = node.source.value - } else { + } else if (node.type === 'TSImportEqualsDeclaration') { if (node.moduleReference.type === 'TSExternalModuleReference') { name = `${node.moduleReference.expression.value}` } else { name = sourceCode.text.slice(...node.moduleReference.range) } + } else { + let decl = node.declarations[0].init as TSESTree.CallExpression + name = (decl.arguments[0] as TSESTree.Literal).value!.toString() } nodes.push({ @@ -428,6 +450,16 @@ export default createEslintRule, MESSAGE_ID>({ return { TSImportEqualsDeclaration: registerNode, ImportDeclaration: registerNode, + VariableDeclaration: node => { + if ( + node.declarations[0].init && + node.declarations[0].init.type === 'CallExpression' && + node.declarations[0].init.callee.type === 'Identifier' && + node.declarations[0].init.callee.name === 'require' + ) { + registerNode(node) + } + }, 'Program:exit': () => { let hasContentBetweenNodes = ( left: SortingNode, diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts index 89e44c225..273fc6e2c 100644 --- a/test/sort-imports.test.ts +++ b/test/sort-imports.test.ts @@ -1203,6 +1203,175 @@ describe(ruleName, () => { }, ], }) + + ruleTester.run(`${ruleName}(${type}): sorts require imports`, rule, { + valid: [ + { + code: dedent` + const { a1, a2 } = require('a') + const { b1 } = require('b') + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + const { b1 } = require('b') + const { a1, a2 } = require('a') + `, + output: dedent` + const { a1, a2 } = require('a') + const { b1 } = require('b') + `, + options: [options], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): sorts require imports by groups`, + rule, + { + valid: [ + { + code: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e1 } = require('e/a') + const { e2 } = require('e/b') + const fs = require('fs') + const path = require('path') + + const { b1, b2 } = require('~/b') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const a = require('.') + const h = require('../../h') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + }, + ], + invalid: [ + { + code: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e2 } = require('e/b') + const { e1 } = require('e/a') + const path = require('path') + + const { b1, b2 } = require('~/b') + const fs = require('fs') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const h = require('../../h') + + const a = require('.') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + output: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e1 } = require('e/a') + const { e2 } = require('e/b') + const fs = require('fs') + const path = require('path') + + const { b1, b2 } = require('~/b') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const a = require('.') + const h = require('../../h') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'e/b', + right: 'e/a', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '~/b', + right: 'fs', + }, + }, + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'fs', + right: '~/c', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '../../h', + right: '.', + }, + }, + { + messageId: 'extraSpacingBetweenImports', + data: { + left: '../../h', + right: '.', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: sorting by natural order`, () => { @@ -2390,6 +2559,175 @@ describe(ruleName, () => { }, ], }) + + ruleTester.run(`${ruleName}(${type}): sorts require imports`, rule, { + valid: [ + { + code: dedent` + const { a1, a2 } = require('a') + const { b1 } = require('b') + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + const { b1 } = require('b') + const { a1, a2 } = require('a') + `, + output: dedent` + const { a1, a2 } = require('a') + const { b1 } = require('b') + `, + options: [options], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): sorts require imports by groups`, + rule, + { + valid: [ + { + code: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e1 } = require('e/a') + const { e2 } = require('e/b') + const fs = require('fs') + const path = require('path') + + const { b1, b2 } = require('~/b') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const a = require('.') + const h = require('../../h') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + }, + ], + invalid: [ + { + code: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e2 } = require('e/b') + const { e1 } = require('e/a') + const path = require('path') + + const { b1, b2 } = require('~/b') + const fs = require('fs') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const h = require('../../h') + + const a = require('.') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + output: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e1 } = require('e/a') + const { e2 } = require('e/b') + const fs = require('fs') + const path = require('path') + + const { b1, b2 } = require('~/b') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const a = require('.') + const h = require('../../h') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'e/b', + right: 'e/a', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '~/b', + right: 'fs', + }, + }, + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'fs', + right: '~/c', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '../../h', + right: '.', + }, + }, + { + messageId: 'extraSpacingBetweenImports', + data: { + left: '../../h', + right: '.', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: sorting by line length`, () => { @@ -3665,6 +4003,182 @@ describe(ruleName, () => { }, ], }) + + ruleTester.run(`${ruleName}(${type}): sorts require imports`, rule, { + valid: [ + { + code: dedent` + const { a1, a2 } = require('a') + const { b1 } = require('b') + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + const { b1 } = require('b') + const { a1, a2 } = require('a') + `, + output: dedent` + const { a1, a2 } = require('a') + const { b1 } = require('b') + `, + options: [options], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): sorts require imports by groups`, + rule, + { + valid: [ + { + code: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e1 } = require('e/a') + const { e2 } = require('e/b') + const path = require('path') + const fs = require('fs') + + const { i1, i2, i3 } = require('~/i') + const { b1, b2 } = require('~/b') + const { c1 } = require('~/c') + + const { K, L, M } = require('../k') + const { j } = require('../j') + const h = require('../../h') + const a = require('.') + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + }, + ], + invalid: [ + { + code: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e2 } = require('e/b') + const { e1 } = require('e/a') + const path = require('path') + + const { b1, b2 } = require('~/b') + const fs = require('fs') + const { c1 } = require('~/c') + const { i1, i2, i3 } = require('~/i') + + const h = require('../../h') + + const a = require('.') + const { j } = require('../j') + const { K, L, M } = require('../k') + `, + output: dedent` + const { c1, c2, c3, c4 } = require('c') + const { e2 } = require('e/b') + const { e1 } = require('e/a') + const path = require('path') + const fs = require('fs') + + const { i1, i2, i3 } = require('~/i') + const { b1, b2 } = require('~/b') + const { c1 } = require('~/c') + + const { K, L, M } = require('../k') + const { j } = require('../j') + const h = require('../../h') + const a = require('.') + `, + options: [ + { + ...options, + newlinesBetween: 'always', + internalPattern: ['~/**'], + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedImportsOrder', + data: { + left: '~/b', + right: 'fs', + }, + }, + { + messageId: 'missedSpacingBetweenImports', + data: { + left: 'fs', + right: '~/c', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '~/c', + right: '~/i', + }, + }, + { + messageId: 'extraSpacingBetweenImports', + data: { + left: '../../h', + right: '.', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '.', + right: '../j', + }, + }, + { + messageId: 'unexpectedImportsOrder', + data: { + left: '../j', + right: '../k', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: misc`, () => {