diff --git a/.changeset/blue-dolls-thank.md b/.changeset/blue-dolls-thank.md new file mode 100644 index 000000000..88ceff73b --- /dev/null +++ b/.changeset/blue-dolls-thank.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": patch +--- + +refactor: migrate all remaining rules diff --git a/.eslintrc.js b/.eslintrc.js index bed02298a..ca744eba3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -77,5 +77,11 @@ module.exports = { 'import-x/default': 0, }, }, + { + files: 'global.d.ts', + rules: { + 'import-x/no-extraneous-dependencies': 0, + }, + }, ], } diff --git a/package.json b/package.json index 881872314..711d4d66c 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@changesets/cli": "^2.27.1", "@eslint/import-test-order-redirect-scoped": "link:./test/fixtures/order-redirect-scoped", "@test-scope/some-module": "link:./test/fixtures/symlinked-module", + "@total-typescript/ts-reset": "^0.5.1", "@types/debug": "^4.1.12", "@types/doctrine": "^0.0.9", "@types/eslint": "^8.56.5", diff --git a/src/core/import-type.ts b/src/core/import-type.ts index 2a304e60b..c0f8c8bf7 100644 --- a/src/core/import-type.ts +++ b/src/core/import-type.ts @@ -177,3 +177,5 @@ function typeTest(name: string, context: RuleContext, path?: string | null) { export function importType(name: string, context: RuleContext) { return typeTest(name, context, resolve(name, context)) } + +export type ImportType = ReturnType diff --git a/src/docs-url.ts b/src/docs-url.ts index 61e029c0a..dd97f7f84 100644 --- a/src/docs-url.ts +++ b/src/docs-url.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - We're using commonjs +// @ts-ignore - The structures of `lib` and `src` are same import pkg from '../package.json' const repoUrl = 'https://github.com/un-es/eslint-plugin-import-x' diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 000000000..69fc8bf0a --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +import '@total-typescript/ts-reset' diff --git a/src/index.ts b/src/index.ts index 89115037b..f735dcbc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,30 @@ import noNamedAsDefault from './rules/no-named-as-default' import noNamedAsDefaultMember from './rules/no-named-as-default-member' import noAnonymousDefaultExport from './rules/no-anonymous-default-export' import noUnusedModules from './rules/no-unused-modules' +import noCommonjs from './rules/no-commonjs' +import noAmd from './rules/no-amd' +import noDuplicates from './rules/no-duplicates' +import first from './rules/first' +import maxDependencies from './rules/max-dependencies' +import noExtraneousDependencies from './rules/no-extraneous-dependencies' +import noAbsolutePath from './rules/no-absolute-path' +import noNodejsModules from './rules/no-nodejs-modules' +import noWebpackLoaderSyntax from './rules/no-webpack-loader-syntax' +import order from './rules/order' +import newlineAfterImport from './rules/newline-after-import' +import preferDefaultExport from './rules/prefer-default-export' +import noDefaultExport from './rules/no-default-export' +import noNamedExport from './rules/no-named-export' +import noDynamicRequire from './rules/no-dynamic-require' +import unambiguous from './rules/unambiguous' +import noUnassignedImport from './rules/no-unassigned-import' +import noUselessPathSegments from './rules/no-useless-path-segments' +import dynamicImportChunkname from './rules/dynamic-import-chunkname' +import noImportModuleExports from './rules/no-import-module-exports' +import noEmptyNamedBlocks from './rules/no-empty-named-blocks' +import exportsLast from './rules/exports-last' +import noDeprecated from './rules/no-deprecated' +import importsFirst from './rules/imports-first' // configs import recommended from './config/recommended' @@ -59,36 +83,36 @@ export const rules = { 'no-anonymous-default-export': noAnonymousDefaultExport, 'no-unused-modules': noUnusedModules, - 'no-commonjs': require('./rules/no-commonjs'), - 'no-amd': require('./rules/no-amd'), - 'no-duplicates': require('./rules/no-duplicates'), - first: require('./rules/first'), - 'max-dependencies': require('./rules/max-dependencies'), - 'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'), - 'no-absolute-path': require('./rules/no-absolute-path'), - 'no-nodejs-modules': require('./rules/no-nodejs-modules'), - 'no-webpack-loader-syntax': require('./rules/no-webpack-loader-syntax'), - order: require('./rules/order'), - 'newline-after-import': require('./rules/newline-after-import'), - 'prefer-default-export': require('./rules/prefer-default-export'), - 'no-default-export': require('./rules/no-default-export'), - 'no-named-export': require('./rules/no-named-export'), - 'no-dynamic-require': require('./rules/no-dynamic-require'), - unambiguous: require('./rules/unambiguous'), - 'no-unassigned-import': require('./rules/no-unassigned-import'), - 'no-useless-path-segments': require('./rules/no-useless-path-segments'), - 'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'), - 'no-import-module-exports': require('./rules/no-import-module-exports'), - 'no-empty-named-blocks': require('./rules/no-empty-named-blocks'), + 'no-commonjs': noCommonjs, + 'no-amd': noAmd, + 'no-duplicates': noDuplicates, + first, + 'max-dependencies': maxDependencies, + 'no-extraneous-dependencies': noExtraneousDependencies, + 'no-absolute-path': noAbsolutePath, + 'no-nodejs-modules': noNodejsModules, + 'no-webpack-loader-syntax': noWebpackLoaderSyntax, + order, + 'newline-after-import': newlineAfterImport, + 'prefer-default-export': preferDefaultExport, + 'no-default-export': noDefaultExport, + 'no-named-export': noNamedExport, + 'no-dynamic-require': noDynamicRequire, + unambiguous, + 'no-unassigned-import': noUnassignedImport, + 'no-useless-path-segments': noUselessPathSegments, + 'dynamic-import-chunkname': dynamicImportChunkname, + 'no-import-module-exports': noImportModuleExports, + 'no-empty-named-blocks': noEmptyNamedBlocks, // export - 'exports-last': require('./rules/exports-last'), + 'exports-last': exportsLast, // metadata-based - 'no-deprecated': require('./rules/no-deprecated'), + 'no-deprecated': noDeprecated, // deprecated aliases to rules - 'imports-first': require('./rules/imports-first'), + 'imports-first': importsFirst, } satisfies Record> export const configs = { diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.ts similarity index 63% rename from src/rules/dynamic-import-chunkname.js rename to src/rules/dynamic-import-chunkname.ts index 31bad3479..c95df990a 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.ts @@ -1,14 +1,29 @@ import vm from 'vm' -import { docsUrl } from '../docs-url' -module.exports = { +import { createRule } from '../utils' +import { TSESTree } from '@typescript-eslint/utils' + +type Options = { + allowEmpty?: boolean + importFunctions?: readonly string[] + webpackChunknameFormat?: string +} + +type MessageId = + | 'leadingComment' + | 'blockComment' + | 'paddedSpaces' + | 'webpackComment' + | 'chunknameFormat' + +export = createRule<[Options?], MessageId>({ + name: 'dynamic-import-chunkname', meta: { type: 'suggestion', docs: { category: 'Style guide', description: 'Enforce a leading comment with the webpackChunkName for dynamic imports.', - url: docsUrl('dynamic-import-chunkname'), }, schema: [ { @@ -30,14 +45,26 @@ module.exports = { }, }, ], + messages: { + leadingComment: + 'dynamic imports require a leading comment with the webpack chunkname', + blockComment: + 'dynamic imports require a /* foo */ style comment, not a // foo comment', + paddedSpaces: + 'dynamic imports require a block comment padded with spaces - /* foo */', + webpackComment: + 'dynamic imports require a "webpack" comment with valid syntax', + chunknameFormat: + 'dynamic imports require a leading comment in the form /*{{format}}*/', + }, }, - + defaultOptions: [], create(context) { - const config = context.options[0] - const { importFunctions = [], allowEmpty = false } = config || {} const { + importFunctions = [], + allowEmpty = false, webpackChunknameFormat = '([0-9a-zA-Z-_/.]|\\[(request|index)\\])+', - } = config || {} + } = context.options[0] || {} const paddedCommentRegex = /^ (\S[\s\S]+\S) $/ const commentStyleRegex = @@ -45,17 +72,14 @@ module.exports = { const chunkSubstrFormat = ` webpackChunkName: ["']${webpackChunknameFormat}["'],? ` const chunkSubstrRegex = new RegExp(chunkSubstrFormat) - function run(node, arg) { + function run(node: TSESTree.Node, arg: TSESTree.Node) { const sourceCode = context.getSourceCode() - const leadingComments = sourceCode.getCommentsBefore - ? sourceCode.getCommentsBefore(arg) // This method is available in ESLint >= 4. - : sourceCode.getComments(arg).leading // This method is deprecated in ESLint 7. + const leadingComments = sourceCode.getCommentsBefore(arg) if ((!leadingComments || leadingComments.length === 0) && !allowEmpty) { context.report({ node, - message: - 'dynamic imports require a leading comment with the webpack chunkname', + messageId: 'leadingComment', }) return } @@ -66,8 +90,7 @@ module.exports = { if (comment.type !== 'Block') { context.report({ node, - message: - 'dynamic imports require a /* foo */ style comment, not a // foo comment', + messageId: 'blockComment', }) return } @@ -75,7 +98,7 @@ module.exports = { if (!paddedCommentRegex.test(comment.value)) { context.report({ node, - message: `dynamic imports require a block comment padded with spaces - /* foo */`, + messageId: 'paddedSpaces', }) return } @@ -86,7 +109,7 @@ module.exports = { } catch (error) { context.report({ node, - message: `dynamic imports require a "webpack" comment with valid syntax`, + messageId: 'webpackComment', }) return } @@ -94,7 +117,7 @@ module.exports = { if (!commentStyleRegex.test(comment.value)) { context.report({ node, - message: `dynamic imports require a "webpack" comment with valid syntax`, + messageId: 'webpackComment', }) return } @@ -107,7 +130,10 @@ module.exports = { if (!isChunknamePresent && !allowEmpty) { context.report({ node, - message: `dynamic imports require a leading comment in the form /*${chunkSubstrFormat}*/`, + messageId: 'chunknameFormat', + data: { + format: chunkSubstrFormat, + }, }) } } @@ -119,7 +145,9 @@ module.exports = { CallExpression(node) { if ( + // @ts-expect-error - legacy parser type node.callee.type !== 'Import' && + 'name' in node.callee && importFunctions.indexOf(node.callee.name) < 0 ) { return @@ -129,4 +157,4 @@ module.exports = { }, } }, -} +}) diff --git a/src/rules/exports-last.js b/src/rules/exports-last.ts similarity index 68% rename from src/rules/exports-last.js rename to src/rules/exports-last.ts index 0612cf32a..f56bcb3df 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.ts @@ -1,6 +1,8 @@ -import { docsUrl } from '../docs-url' +import type { TSESTree } from '@typescript-eslint/utils' -function isNonExportStatement({ type }) { +import { createRule } from '../utils' + +function isNonExportStatement({ type }: TSESTree.Node) { return ( type !== 'ExportDefaultDeclaration' && type !== 'ExportNamedDeclaration' && @@ -8,17 +10,20 @@ function isNonExportStatement({ type }) { ) } -module.exports = { +export = createRule({ + name: 'exports-last', meta: { type: 'suggestion', docs: { category: 'Style guide', description: 'Ensure all exports appear after other statements.', - url: docsUrl('exports-last'), }, schema: [], + messages: { + end: 'Export statements should appear at the end of the file', + }, }, - + defaultOptions: [], create(context) { return { Program({ body }) { @@ -30,8 +35,7 @@ module.exports = { if (!isNonExportStatement(node)) { context.report({ node, - message: - 'Export statements should appear at the end of the file', + messageId: 'end', }) } }) @@ -39,4 +43,4 @@ module.exports = { }, } }, -} +}) diff --git a/src/rules/first.js b/src/rules/first.ts similarity index 57% rename from src/rules/first.js rename to src/rules/first.ts index 99148b0ff..1aa3e7d9c 100644 --- a/src/rules/first.js +++ b/src/rules/first.ts @@ -1,18 +1,32 @@ -import { docsUrl } from '../docs-url' +import { TSESLint, TSESTree } from '@typescript-eslint/utils' +import { createRule } from '../utils' -function getImportValue(node) { +function getImportValue(node: TSESTree.ProgramStatement) { return node.type === 'ImportDeclaration' ? node.source.value - : node.moduleReference.expression.value + : 'moduleReference' in node && + 'expression' in node.moduleReference && + 'value' in node.moduleReference.expression && + node.moduleReference.expression.value } -module.exports = { +function isPossibleDirective(node: TSESTree.ProgramStatement) { + return ( + node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && + typeof node.expression.value === 'string' + ) +} + +type MessageId = 'absolute' | 'order' + +export = createRule<['absolute-first'?], MessageId>({ + name: 'first', meta: { type: 'suggestion', docs: { category: 'Style guide', description: 'Ensure all imports appear before other statements.', - url: docsUrl('first'), }, fixable: 'code', schema: [ @@ -21,34 +35,38 @@ module.exports = { enum: ['absolute-first', 'disable-absolute-first'], }, ], + messages: { + absolute: 'Absolute imports should come before relative imports.', + order: 'Import in body of module; reorder to top.', + }, }, - + defaultOptions: [], create(context) { - function isPossibleDirective(node) { - return ( - node.type === 'ExpressionStatement' && - node.expression.type === 'Literal' && - typeof node.expression.value === 'string' - ) - } - return { Program(n) { const body = n.body - if (!body) { + if (!body?.length) { return } + const absoluteFirst = context.options[0] === 'absolute-first' - const message = 'Import in body of module; reorder to top.' const sourceCode = context.getSourceCode() const originSourceCode = sourceCode.getText() + let nonImportCount = 0 let anyExpressions = false let anyRelative = false - let lastLegalImp = null - const errorInfos = [] + + let lastLegalImp: TSESTree.ProgramStatement | null = null + + const errorInfos: Array<{ + node: TSESTree.ProgramStatement + range: [number, number] + }> = [] + let shouldSort = true let lastSortNodesIndex = 0 + body.forEach(function (node, index) { if (!anyExpressions && isPossibleDirective(node)) { return @@ -61,7 +79,8 @@ module.exports = { node.type === 'TSImportEqualsDeclaration' ) { if (absoluteFirst) { - if (/^\./.test(getImportValue(node))) { + const importValue = getImportValue(node) + if (typeof importValue === 'string' && /^\./.test(importValue)) { anyRelative = true } else if (anyRelative) { context.report({ @@ -69,27 +88,29 @@ module.exports = { node.type === 'ImportDeclaration' ? node.source : node.moduleReference, - message: - 'Absolute imports should come before relative imports.', + messageId: 'absolute', }) } } + if (nonImportCount > 0) { for (const variable of context.getDeclaredVariables(node)) { if (!shouldSort) { break } - const references = variable.references - if (references.length) { - for (const reference of references) { - if (reference.identifier.range[0] < node.range[1]) { - shouldSort = false - break - } + + for (const reference of variable.references) { + if (reference.identifier.range[0] < node.range[1]) { + shouldSort = false + break } } } - shouldSort && (lastSortNodesIndex = errorInfos.length) + + if (shouldSort) { + lastSortNodesIndex = errorInfos.length + } + errorInfos.push({ node, range: [body[index - 1].range[1], node.range[1]], @@ -101,49 +122,49 @@ module.exports = { nonImportCount++ } }) + if (!errorInfos.length) { return } - errorInfos.forEach(function (errorInfo, index) { - const node = errorInfo.node - const infos = { - node, - message, - } + + errorInfos.forEach(({ node }, index) => { + let fix: TSESLint.ReportFixFunction | undefined + if (index < lastSortNodesIndex) { - infos.fix = function (fixer) { - return fixer.insertTextAfter(node, '') - } + fix = (fixer: TSESLint.RuleFixer) => fixer.insertTextAfter(node, '') } else if (index === lastSortNodesIndex) { const sortNodes = errorInfos.slice(0, lastSortNodesIndex + 1) - infos.fix = function (fixer) { - const removeFixers = sortNodes.map(function (_errorInfo) { - return fixer.removeRange(_errorInfo.range) - }) - const range = [0, removeFixers[removeFixers.length - 1].range[1]] + fix = (fixer: TSESLint.RuleFixer) => { + const removeFixers = sortNodes.map(({ range }) => + fixer.removeRange(range), + ) + const range = [ + 0, + removeFixers[removeFixers.length - 1].range[1], + ] as const + let insertSourceCode = sortNodes - .map(function (_errorInfo) { - const nodeSourceCode = String.prototype.slice.apply( - originSourceCode, - _errorInfo.range, - ) + .map(({ range }) => { + const nodeSourceCode = originSourceCode.slice(...range) if (/\S/.test(nodeSourceCode[0])) { return `\n${nodeSourceCode}` } return nodeSourceCode }) .join('') - let insertFixer = null + let replaceSourceCode = '' + if (!lastLegalImp) { insertSourceCode = - insertSourceCode.trim() + insertSourceCode.match(/^(\s+)/)[0] + insertSourceCode.trim() + insertSourceCode.match(/^(\s+)/)![0] } - insertFixer = lastLegalImp + + const insertFixer = lastLegalImp ? fixer.insertTextAfter(lastLegalImp, insertSourceCode) : fixer.insertTextBefore(body[0], insertSourceCode) - const fixers = [insertFixer].concat(removeFixers) + const fixers = [insertFixer, ...removeFixers] fixers.forEach((computedFixer, i) => { replaceSourceCode += originSourceCode.slice( @@ -155,9 +176,13 @@ module.exports = { return fixer.replaceTextRange(range, replaceSourceCode) } } - context.report(infos) + context.report({ + node, + messageId: 'order', + fix, + }) }) }, } }, -} +}) diff --git a/src/rules/imports-first.js b/src/rules/imports-first.js deleted file mode 100644 index 620d5f7e3..000000000 --- a/src/rules/imports-first.js +++ /dev/null @@ -1,15 +0,0 @@ -import { docsUrl } from '../docs-url' - -const first = require('./first') - -const newMeta = { - ...first.meta, - deprecated: true, - docs: { - category: 'Style guide', - description: 'Replaced by `import-x/first`.', - url: docsUrl('imports-first', '7b25c1cb95ee18acc1531002fd343e1e6031f9ed'), - }, -} - -module.exports = { ...first, meta: newMeta } diff --git a/src/rules/imports-first.ts b/src/rules/imports-first.ts new file mode 100644 index 000000000..63b4642eb --- /dev/null +++ b/src/rules/imports-first.ts @@ -0,0 +1,22 @@ +import { ESLintUtils } from '@typescript-eslint/utils' + +import { docsUrl } from '../docs-url' + +import first from './first' + +const createRule = ESLintUtils.RuleCreator(ruleName => + docsUrl(ruleName, '7b25c1cb95ee18acc1531002fd343e1e6031f9ed'), +) + +export = createRule({ + ...first, + name: 'imports-first', + meta: { + ...first.meta, + deprecated: true, + docs: { + category: 'Style guide', + description: 'Replaced by `import-x/first`.', + }, + }, +}) diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js deleted file mode 100644 index b8df2580b..000000000 --- a/src/rules/max-dependencies.js +++ /dev/null @@ -1,63 +0,0 @@ -import { moduleVisitor } from '../utils/module-visitor' -import { docsUrl } from '../docs-url' - -const DEFAULT_MAX = 10 -const DEFAULT_IGNORE_TYPE_IMPORTS = false -const TYPE_IMPORT = 'type' - -const countDependencies = (dependencies, lastNode, context) => { - const { max } = context.options[0] || { max: DEFAULT_MAX } - - if (dependencies.size > max) { - context.report( - lastNode, - `Maximum number of dependencies (${max}) exceeded.`, - ) - } -} - -module.exports = { - meta: { - type: 'suggestion', - docs: { - category: 'Style guide', - description: - 'Enforce the maximum number of dependencies a module can have.', - url: docsUrl('max-dependencies'), - }, - - schema: [ - { - type: 'object', - properties: { - max: { type: 'number' }, - ignoreTypeImports: { type: 'boolean' }, - }, - additionalProperties: false, - }, - ], - }, - - create(context) { - const { ignoreTypeImports = DEFAULT_IGNORE_TYPE_IMPORTS } = - context.options[0] || {} - - const dependencies = new Set() // keep track of dependencies - let lastNode // keep track of the last node to report on - - return { - 'Program:exit'() { - countDependencies(dependencies, lastNode, context) - }, - ...moduleVisitor( - (source, { importKind }) => { - if (importKind !== TYPE_IMPORT || !ignoreTypeImports) { - dependencies.add(source.value) - } - lastNode = source - }, - { commonjs: true }, - ), - } - }, -} diff --git a/src/rules/max-dependencies.ts b/src/rules/max-dependencies.ts new file mode 100644 index 000000000..81889337a --- /dev/null +++ b/src/rules/max-dependencies.ts @@ -0,0 +1,74 @@ +import type { TSESTree } from '@typescript-eslint/utils' + +import { moduleVisitor } from '../utils/module-visitor' +import { createRule } from '../utils' + +type Options = { + ignoreTypeImports?: boolean + max?: number +} + +type MessageId = 'max' + +export = createRule<[Options?], MessageId>({ + name: 'max-dependencies', + meta: { + type: 'suggestion', + docs: { + category: 'Style guide', + description: + 'Enforce the maximum number of dependencies a module can have.', + }, + schema: [ + { + type: 'object', + properties: { + max: { type: 'number' }, + ignoreTypeImports: { type: 'boolean' }, + }, + additionalProperties: false, + }, + ], + messages: { + max: 'Maximum number of dependencies ({{max}}) exceeded.', + }, + }, + defaultOptions: [], + create(context) { + const { ignoreTypeImports } = context.options[0] || {} + + const dependencies = new Set() // keep track of dependencies + + let lastNode: TSESTree.StringLiteral // keep track of the last node to report on + + return { + 'Program:exit'() { + const { max = 10 } = context.options[0] || {} + + if (dependencies.size <= max) { + return + } + + context.report({ + node: lastNode, + messageId: 'max', + data: { + max, + }, + }) + }, + ...moduleVisitor( + (source, node) => { + if ( + ('importKind' in node && node.importKind !== 'type') || + !ignoreTypeImports + ) { + dependencies.add(source.value) + } + lastNode = source + }, + { commonjs: true }, + ), + } + }, +}) diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.ts similarity index 69% rename from src/rules/newline-after-import.js rename to src/rules/newline-after-import.ts index b9d2c719a..2e7158318 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.ts @@ -1,72 +1,95 @@ /** * Rule to enforce new line after import not followed by another import. */ +import { TSESLint, TSESTree } from '@typescript-eslint/utils' +import debug from 'debug' import { isStaticRequire } from '../core/static-require' -import { docsUrl } from '../docs-url' -import debug from 'debug' +import { createRule } from '../utils' + const log = debug('eslint-plugin-import-x:rules:newline-after-import') -function containsNodeOrEqual(outerNode, innerNode) { +function containsNodeOrEqual( + outerNode: TSESTree.Node, + innerNode: TSESTree.Node, +) { return ( outerNode.range[0] <= innerNode.range[0] && outerNode.range[1] >= innerNode.range[1] ) } -function getScopeBody(scope) { +function getScopeBody(scope: TSESLint.Scope.Scope) { if (scope.block.type === 'SwitchStatement') { log('SwitchStatement scopes not supported') - return null + return [] } - const { body } = scope.block - if (body && body.type === 'BlockStatement') { + const body = 'body' in scope.block ? scope.block.body : null + + if (body && 'type' in body && body.type === 'BlockStatement') { return body.body } - return body + return Array.isArray(body) ? body : [] } -function findNodeIndexInScopeBody(body, nodeToFind) { +function findNodeIndexInScopeBody( + body: TSESTree.ProgramStatement[], + nodeToFind: TSESTree.Node, +) { return body.findIndex(node => containsNodeOrEqual(node, nodeToFind)) } -function getLineDifference(node, nextNode) { +function getLineDifference( + node: TSESTree.Node, + nextNode: TSESTree.Comment | TSESTree.Node, +) { return nextNode.loc.start.line - node.loc.end.line } -function isClassWithDecorator(node) { - return ( - node.type === 'ClassDeclaration' && - node.decorators && - node.decorators.length - ) +function isClassWithDecorator( + node: TSESTree.Node, +): node is TSESTree.ClassDeclaration & { decorators: TSESTree.Decorator[] } { + return node.type === 'ClassDeclaration' && !!node.decorators?.length } -function isExportDefaultClass(node) { +function isExportDefaultClass( + node: TSESTree.Node, +): node is TSESTree.ExportDefaultDeclaration { return ( node.type === 'ExportDefaultDeclaration' && node.declaration.type === 'ClassDeclaration' ) } -function isExportNameClass(node) { +function isExportNameClass( + node: TSESTree.Node, +): node is TSESTree.ExportNamedDeclaration & { + declaration: TSESTree.ClassDeclaration +} { return ( node.type === 'ExportNamedDeclaration' && - node.declaration && - node.declaration.type === 'ClassDeclaration' + node.declaration?.type === 'ClassDeclaration' ) } -module.exports = { +type Options = { + count?: number + exactCount?: boolean + considerComments?: boolean +} + +type MessageId = 'newline' + +export = createRule<[Options?], MessageId>({ + name: 'newline-after-import', meta: { type: 'layout', docs: { category: 'Style guide', description: 'Enforce a newline after import statements.', - url: docsUrl('newline-after-import'), }, fixable: 'whitespace', schema: [ @@ -83,10 +106,17 @@ module.exports = { additionalProperties: false, }, ], + messages: { + newline: + 'Expected {{count}} empty line{{lineSuffix}} after {{type}} statement not followed by another {{type}}.', + }, }, + defaultOptions: [], create(context) { let level = 0 - const requireCalls = [] + + const requireCalls: TSESTree.CallExpression[] = [] + const options = { count: 1, exactCount: false, @@ -94,7 +124,11 @@ module.exports = { ...context.options[0], } - function checkForNewLine(node, nextNode, type) { + function checkForNewLine( + node: TSESTree.Statement, + nextNode: TSESTree.Node, + type: 'import' | 'require', + ) { if (isExportDefaultClass(nextNode) || isExportNameClass(nextNode)) { const classNode = nextNode.declaration @@ -123,7 +157,12 @@ module.exports = { line: node.loc.end.line, column, }, - message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} after ${type} statement not followed by another ${type}.`, + messageId: 'newline', + data: { + count: options.count, + lineSuffix: options.count > 1 ? 's' : '', + type, + }, fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? undefined @@ -136,7 +175,10 @@ module.exports = { } } - function commentAfterImport(node, nextComment) { + function commentAfterImport( + node: TSESTree.Node, + nextComment: TSESTree.Comment, + ) { const lineDifference = getLineDifference(node, nextComment) const EXPECTED_LINE_DIFFERENCE = options.count + 1 @@ -152,7 +194,12 @@ module.exports = { line: node.loc.end.line, column, }, - message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} after import statement not followed by another import.`, + messageId: 'newline', + data: { + count: options.count, + lineSuffix: options.count > 1 ? 's' : '', + type: 'import', + }, fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? undefined @@ -172,20 +219,25 @@ module.exports = { level-- } - function checkImport(node) { + function checkImport( + node: TSESTree.ImportDeclaration | TSESTree.TSImportEqualsDeclaration, + ) { const { parent } = node - if (!parent || !parent.body) { + if (!parent || !('body' in parent) || !parent.body) { return } - const nodePosition = parent.body.indexOf(node) - const nextNode = parent.body[nodePosition + 1] + const root = parent as TSESTree.Program + + const nodePosition = root.body.indexOf(node) + const nextNode = root.body[nodePosition + 1] const endLine = node.loc.end.line - let nextComment - if (typeof parent.comments !== 'undefined' && options.considerComments) { - nextComment = parent.comments.find( + let nextComment: TSESTree.Comment | undefined + + if (typeof root.comments !== 'undefined' && options.considerComments) { + nextComment = root.comments.find( o => o.loc.start.line >= endLine && o.loc.start.line <= endLine + options.count + 1, @@ -224,10 +276,12 @@ module.exports = { : context.getFilename(), ) const scopeBody = getScopeBody(context.getScope()) + log('got scope:', scopeBody) requireCalls.forEach((node, index) => { const nodePosition = findNodeIndexInScopeBody(scopeBody, node) + log('node position in scope:', nodePosition) const statementWithRequireCall = scopeBody[nodePosition] @@ -264,4 +318,4 @@ module.exports = { 'Decorator:exit': decrementLevel, } }, -} +}) diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js deleted file mode 100644 index 563840032..000000000 --- a/src/rules/no-absolute-path.js +++ /dev/null @@ -1,45 +0,0 @@ -import path from 'path' -import { moduleVisitor, makeOptionsSchema } from '../utils/module-visitor' -import { isAbsolute } from '../core/import-type' -import { docsUrl } from '../docs-url' - -module.exports = { - meta: { - type: 'suggestion', - docs: { - category: 'Static analysis', - description: 'Forbid import of modules using absolute paths.', - url: docsUrl('no-absolute-path'), - }, - fixable: 'code', - schema: [makeOptionsSchema()], - }, - - create(context) { - function reportIfAbsolute(source) { - if (isAbsolute(source.value)) { - context.report({ - node: source, - message: 'Do not import modules using an absolute path', - fix(fixer) { - const resolvedContext = context.getPhysicalFilename - ? context.getPhysicalFilename() - : context.getFilename() - // node.js and web imports work with posix style paths ("/") - let relativePath = path.posix.relative( - path.dirname(resolvedContext), - source.value, - ) - if (!relativePath.startsWith('.')) { - relativePath = `./${relativePath}` - } - return fixer.replaceText(source, JSON.stringify(relativePath)) - }, - }) - } - } - - const options = { esmodule: true, commonjs: true, ...context.options[0] } - return moduleVisitor(reportIfAbsolute, options) - }, -} diff --git a/src/rules/no-absolute-path.ts b/src/rules/no-absolute-path.ts new file mode 100644 index 000000000..75d83cc49 --- /dev/null +++ b/src/rules/no-absolute-path.ts @@ -0,0 +1,54 @@ +import path from 'path' + +import { + moduleVisitor, + makeOptionsSchema, + type ModuleOptions, +} from '../utils/module-visitor' +import { isAbsolute } from '../core/import-type' +import { createRule } from '../utils' + +type MessageId = 'absolute' + +export = createRule<[ModuleOptions?], MessageId>({ + name: 'no-absolute-path', + meta: { + type: 'suggestion', + docs: { + category: 'Static analysis', + description: 'Forbid import of modules using absolute paths.', + }, + fixable: 'code', + schema: [makeOptionsSchema()], + messages: { + absolute: 'Do not import modules using an absolute path', + }, + }, + defaultOptions: [], + create(context) { + const options = { esmodule: true, commonjs: true, ...context.options[0] } + return moduleVisitor(source => { + if (!isAbsolute(source.value)) { + return + } + context.report({ + node: source, + messageId: 'absolute', + fix(fixer) { + const resolvedContext = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename() + // node.js and web imports work with posix style paths ("/") + let relativePath = path.posix.relative( + path.dirname(resolvedContext), + source.value, + ) + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}` + } + return fixer.replaceText(source, JSON.stringify(relativePath)) + }, + }) + }, options) + }, +}) diff --git a/src/rules/no-amd.js b/src/rules/no-amd.ts similarity index 71% rename from src/rules/no-amd.js rename to src/rules/no-amd.ts index 8f7010ad2..fac2c7145 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.ts @@ -2,19 +2,24 @@ * Rule to prefer imports to AMD */ -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' -module.exports = { +type MessageId = 'amd' + +export = createRule<[], MessageId>({ + name: 'no-amd', meta: { type: 'suggestion', docs: { category: 'Module systems', description: 'Forbid AMD `require` and `define` calls.', - url: docsUrl('no-amd'), }, schema: [], + messages: { + amd: 'Expected imports instead of AMD {{type}}().', + }, }, - + defaultOptions: [], create(context) { return { CallExpression(node) { @@ -25,6 +30,7 @@ module.exports = { if (node.callee.type !== 'Identifier') { return } + if (node.callee.name !== 'require' && node.callee.name !== 'define') { return } @@ -35,17 +41,21 @@ module.exports = { } const modules = node.arguments[0] + if (modules.type !== 'ArrayExpression') { return } // todo: check second arg type? (identifier or callback) - context.report( + context.report({ node, - `Expected imports instead of AMD ${node.callee.name}().`, - ) + messageId: 'amd', + data: { + type: node.callee.name, + }, + }) }, } }, -} +}) diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.ts similarity index 51% rename from src/rules/no-commonjs.js rename to src/rules/no-commonjs.ts index eda6d4b50..fe65fbc81 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.ts @@ -2,42 +2,45 @@ * Rule to prefer ES6 to CJS */ -import { docsUrl } from '../docs-url' +import { TSESLint, TSESTree } from '@typescript-eslint/utils' +import { createRule } from '../utils' -const EXPORT_MESSAGE = 'Expected "export" or "export default"' -const IMPORT_MESSAGE = 'Expected "import" instead of "require()"' +type NormalizedOptions = { + allowPrimitiveModules?: boolean + allowRequire?: boolean + allowConditionalRequire?: boolean +} + +type Options = 'allow-primitive-modules' | NormalizedOptions -function normalizeLegacyOptions(options) { - if (options.indexOf('allow-primitive-modules') >= 0) { +type MessageId = 'export' | 'import' + +function normalizeLegacyOptions(options: [Options?]): NormalizedOptions { + if (options.includes('allow-primitive-modules')) { return { allowPrimitiveModules: true } } - return options[0] || {} + return (options[0] as NormalizedOptions) || {} } -function allowPrimitive(node, options) { +function allowPrimitive( + node: TSESTree.MemberExpression, + options: NormalizedOptions, +) { if (!options.allowPrimitiveModules) { return false } - if (node.parent.type !== 'AssignmentExpression') { + if (node.parent!.type !== 'AssignmentExpression') { return false } - return node.parent.right.type !== 'ObjectExpression' -} - -function allowRequire(node, options) { - return options.allowRequire -} - -function allowConditionalRequire(node, options) { - return options.allowConditionalRequire !== false + return node.parent!.right.type !== 'ObjectExpression' } -function validateScope(scope) { +function validateScope(scope: TSESLint.Scope.Scope) { return scope.variableScope.type === 'module' } // https://github.com/estree/estree/blob/HEAD/es5.md -function isConditional(node) { +function isConditional(node: TSESTree.Node) { if ( node.type === 'IfStatement' || node.type === 'TryStatement' || @@ -52,70 +55,77 @@ function isConditional(node) { return false } -function isLiteralString(node) { +function isLiteralString(node: TSESTree.CallExpressionArgument) { return ( (node.type === 'Literal' && typeof node.value === 'string') || (node.type === 'TemplateLiteral' && node.expressions.length === 0) ) } -const schemaString = { enum: ['allow-primitive-modules'] } -const schemaObject = { - type: 'object', - properties: { - allowPrimitiveModules: { type: 'boolean' }, - allowRequire: { type: 'boolean' }, - allowConditionalRequire: { type: 'boolean' }, - }, - additionalProperties: false, -} - -module.exports = { +export = createRule<[Options?], MessageId>({ + name: 'no-commonjs', meta: { type: 'suggestion', docs: { category: 'Module systems', description: 'Forbid CommonJS `require` calls and `module.exports` or `exports.*`.', - url: docsUrl('no-commonjs'), }, - schema: { anyOf: [ { type: 'array', - items: [schemaString], + items: [{ enum: ['allow-primitive-modules'] }], additionalItems: false, }, { type: 'array', - items: [schemaObject], + items: [ + { + type: 'object', + properties: { + allowPrimitiveModules: { type: 'boolean' }, + allowRequire: { type: 'boolean' }, + allowConditionalRequire: { type: 'boolean' }, + }, + additionalProperties: false, + }, + ], additionalItems: false, }, ], }, + messages: { + export: 'Expected "export" or "export default"', + import: 'Expected "import" instead of "require()"', + }, }, - + defaultOptions: [], create(context) { const options = normalizeLegacyOptions(context.options) return { MemberExpression(node) { // module.exports - if (node.object.name === 'module' && node.property.name === 'exports') { + if ( + 'name' in node.object && + node.object.name === 'module' && + 'name' in node.property && + node.property.name === 'exports' + ) { if (allowPrimitive(node, options)) { return } - context.report({ node, message: EXPORT_MESSAGE }) + context.report({ node, messageId: 'export' }) } // exports. - if (node.object.name === 'exports') { + if ('name' in node.object && node.object.name === 'exports') { const isInScope = context .getScope() .variables.some(variable => variable.name === 'exports') if (!isInScope) { - context.report({ node, message: EXPORT_MESSAGE }) + context.report({ node, messageId: 'export' }) } } }, @@ -138,13 +148,13 @@ module.exports = { return } - if (allowRequire(call, options)) { + if (options.allowRequire) { return } if ( - allowConditionalRequire(call, options) && - isConditional(call.parent) + options.allowConditionalRequire !== false && + isConditional(call.parent!) ) { return } @@ -152,9 +162,9 @@ module.exports = { // keeping it simple: all 1-string-arg `require` calls are reported context.report({ node: call.callee, - message: IMPORT_MESSAGE, + messageId: 'import', }) }, } }, -} +}) diff --git a/src/rules/no-default-export.js b/src/rules/no-default-export.js deleted file mode 100644 index 24ad661ff..000000000 --- a/src/rules/no-default-export.js +++ /dev/null @@ -1,49 +0,0 @@ -import { docsUrl } from '../docs-url' - -module.exports = { - meta: { - type: 'suggestion', - docs: { - category: 'Style guide', - description: 'Forbid default exports.', - url: docsUrl('no-default-export'), - }, - schema: [], - }, - - create(context) { - // ignore non-modules - if (context.parserOptions.sourceType !== 'module') { - return {} - } - - const preferNamed = 'Prefer named exports.' - const noAliasDefault = ({ local }) => - `Do not alias \`${local.name}\` as \`default\`. Just export \`${local.name}\` itself instead.` - - return { - ExportDefaultDeclaration(node) { - const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {} - context.report({ node, message: preferNamed, loc }) - }, - - ExportNamedDeclaration(node) { - node.specifiers - .filter( - specifier => - (specifier.exported.name || specifier.exported.value) === - 'default', - ) - .forEach(specifier => { - const { loc } = - context.getSourceCode().getFirstTokens(node)[1] || {} - if (specifier.type === 'ExportDefaultSpecifier') { - context.report({ node, message: preferNamed, loc }) - } else if (specifier.type === 'ExportSpecifier') { - context.report({ node, message: noAliasDefault(specifier), loc }) - } - }) - }, - } - }, -} diff --git a/src/rules/no-default-export.ts b/src/rules/no-default-export.ts new file mode 100644 index 000000000..5a967b2c8 --- /dev/null +++ b/src/rules/no-default-export.ts @@ -0,0 +1,67 @@ +import { createRule } from '../utils' + +export = createRule({ + name: 'no-default-export', + meta: { + type: 'suggestion', + docs: { + category: 'Style guide', + description: 'Forbid default exports.', + }, + schema: [], + messages: { + preferNamed: 'Prefer named exports.', + noAliasDefault: + 'Do not alias `{{local}}` as `default`. Just export `{{local}}` itself instead.', + }, + }, + defaultOptions: [], + create(context) { + // ignore non-modules + if (context.parserOptions.sourceType !== 'module') { + return {} + } + + return { + ExportDefaultDeclaration(node) { + const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {} + context.report({ + node, + messageId: 'preferNamed', + loc, + }) + }, + + ExportNamedDeclaration(node) { + node.specifiers + .filter( + specifier => + (specifier.exported.name || + ('value' in specifier.exported && specifier.exported.value)) === + 'default', + ) + .forEach(specifier => { + const { loc } = + context.getSourceCode().getFirstTokens(node)[1] || {} + // @ts-expect-error - legacy parser type + if (specifier.type === 'ExportDefaultSpecifier') { + context.report({ + node, + messageId: 'preferNamed', + loc, + }) + } else if (specifier.type === 'ExportSpecifier') { + context.report({ + node, + messageId: 'noAliasDefault', + data: { + local: specifier.local.name, + }, + loc, + }) + } + }) + }, + } + }, +}) diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js deleted file mode 100644 index 7cab95609..000000000 --- a/src/rules/no-deprecated.js +++ /dev/null @@ -1,181 +0,0 @@ -import { declaredScope } from '../utils/declared-scope' -import { ExportMap } from '../export-map' -import { docsUrl } from '../docs-url' - -function message(deprecation) { - return `Deprecated${deprecation.description ? `: ${deprecation.description}` : '.'}` -} - -function getDeprecation(metadata) { - if (!metadata || !metadata.doc) { - return - } - - return metadata.doc.tags.find(t => t.title === 'deprecated') -} - -module.exports = { - meta: { - type: 'suggestion', - docs: { - category: 'Helpful warnings', - description: - 'Forbid imported names marked with `@deprecated` documentation tag.', - url: docsUrl('no-deprecated'), - }, - schema: [], - }, - - create(context) { - const deprecated = new Map() - const namespaces = new Map() - - function checkSpecifiers(node) { - if (node.type !== 'ImportDeclaration') { - return - } - if (node.source == null) { - return - } // local export, ignore - - const imports = ExportMap.get(node.source.value, context) - if (imports == null) { - return - } - - const moduleDeprecation = - imports.doc && imports.doc.tags.find(t => t.title === 'deprecated') - if (moduleDeprecation) { - context.report({ node, message: message(moduleDeprecation) }) - } - - if (imports.errors.length) { - imports.reportErrors(context, node) - return - } - - node.specifiers.forEach(function (im) { - let imported - let local - switch (im.type) { - case 'ImportNamespaceSpecifier': { - if (!imports.size) { - return - } - namespaces.set(im.local.name, imports) - return - } - - case 'ImportDefaultSpecifier': - imported = 'default' - local = im.local.name - break - - case 'ImportSpecifier': - imported = im.imported.name - local = im.local.name - break - - default: - return // can't handle this one - } - - // unknown thing can't be deprecated - const exported = imports.get(imported) - if (exported == null) { - return - } - - // capture import of deep namespace - if (exported.namespace) { - namespaces.set(local, exported.namespace) - } - - const deprecation = getDeprecation(imports.get(imported)) - if (!deprecation) { - return - } - - context.report({ node: im, message: message(deprecation) }) - - deprecated.set(local, deprecation) - }) - } - - return { - Program: ({ body }) => body.forEach(checkSpecifiers), - - Identifier(node) { - if ( - node.parent.type === 'MemberExpression' && - node.parent.property === node - ) { - return // handled by MemberExpression - } - - // ignore specifier identifiers - if (node.parent.type.slice(0, 6) === 'Import') { - return - } - - if (!deprecated.has(node.name)) { - return - } - - if (declaredScope(context, node.name) !== 'module') { - return - } - context.report({ - node, - message: message(deprecated.get(node.name)), - }) - }, - - MemberExpression(dereference) { - if (dereference.object.type !== 'Identifier') { - return - } - if (!namespaces.has(dereference.object.name)) { - return - } - - if (declaredScope(context, dereference.object.name) !== 'module') { - return - } - - // go deep - let namespace = namespaces.get(dereference.object.name) - const namepath = [dereference.object.name] - // while property is namespace and parent is member expression, keep validating - while ( - namespace instanceof ExportMap && - dereference.type === 'MemberExpression' - ) { - // ignore computed parts for now - if (dereference.computed) { - return - } - - const metadata = namespace.get(dereference.property.name) - - if (!metadata) { - break - } - const deprecation = getDeprecation(metadata) - - if (deprecation) { - context.report({ - node: dereference.property, - message: message(deprecation), - }) - } - - // stash and pop - namepath.push(dereference.property.name) - namespace = metadata.namespace - dereference = dereference.parent - } - }, - } - }, -} diff --git a/src/rules/no-deprecated.ts b/src/rules/no-deprecated.ts new file mode 100644 index 000000000..e72c8a315 --- /dev/null +++ b/src/rules/no-deprecated.ts @@ -0,0 +1,208 @@ +import type { TSESTree } from '@typescript-eslint/utils' +import type { Tag } from 'doctrine' + +import { declaredScope } from '../utils/declared-scope' +import { ExportMap, type ModuleNamespace } from '../export-map' +import { createRule } from '../utils' + +function message(deprecation: Tag) { + return { + messageId: 'deprecated', + data: { + description: deprecation.description + ? `: ${deprecation.description}` + : '.', + }, + } as const +} + +function getDeprecation(metadata?: ModuleNamespace | null) { + if (!metadata || !metadata.doc) { + return + } + + return metadata.doc.tags.find(t => t.title === 'deprecated') +} + +export = createRule({ + name: 'no-deprecated', + meta: { + type: 'suggestion', + docs: { + category: 'Helpful warnings', + description: + 'Forbid imported names marked with `@deprecated` documentation tag.', + }, + schema: [], + messages: { + deprecated: 'Deprecated{{description}}', + }, + }, + defaultOptions: [], + create(context) { + const deprecated = new Map() + const namespaces = new Map() + + return { + Program: ({ body }) => + body.forEach(node => { + if (node.type !== 'ImportDeclaration') { + return + } + + if (node.source == null) { + return + } // local export, ignore + + const imports = ExportMap.get(node.source.value, context) + + if (imports == null) { + return + } + + const moduleDeprecation = imports.doc?.tags.find( + t => t.title === 'deprecated', + ) + if (moduleDeprecation) { + context.report({ + node, + ...message(moduleDeprecation), + }) + } + + if (imports.errors.length) { + imports.reportErrors(context, node) + return + } + + node.specifiers.forEach(function (im) { + let imported: string + let local: string + switch (im.type) { + case 'ImportNamespaceSpecifier': { + if (!imports.size) { + return + } + namespaces.set(im.local.name, imports) + return + } + + case 'ImportDefaultSpecifier': + imported = 'default' + local = im.local.name + break + + case 'ImportSpecifier': + imported = im.imported.name + local = im.local.name + break + + default: + return // can't handle this one + } + + // unknown thing can't be deprecated + const exported = imports.get(imported) + if (exported == null) { + return + } + + // capture import of deep namespace + if (exported.namespace) { + namespaces.set(local, exported.namespace) + } + + const deprecation = getDeprecation(imports.get(imported)) + + if (!deprecation) { + return + } + + context.report({ + node: im, + ...message(deprecation), + }) + + deprecated.set(local, deprecation) + }) + }), + + Identifier(node) { + if ( + !node.parent || + (node.parent.type === 'MemberExpression' && + node.parent.property === node) + ) { + return // handled by MemberExpression + } + + // ignore specifier identifiers + if (node.parent.type.slice(0, 6) === 'Import') { + return + } + + if (!deprecated.has(node.name)) { + return + } + + if (declaredScope(context, node.name) !== 'module') { + return + } + context.report({ + node, + ...message(deprecated.get(node.name)), + }) + }, + + MemberExpression(dereference) { + if (dereference.object.type !== 'Identifier') { + return + } + if (!namespaces.has(dereference.object.name)) { + return + } + + if (declaredScope(context, dereference.object.name) !== 'module') { + return + } + + // go deep + let namespace = namespaces.get(dereference.object.name) + const namepath = [dereference.object.name] + + let node: TSESTree.Node | undefined = dereference + + // while property is namespace and parent is member expression, keep validating + while ( + namespace instanceof ExportMap && + node?.type === 'MemberExpression' + ) { + // ignore computed parts for now + if (node.computed) { + return + } + + const metadata = namespace.get(node.property.name) + + if (!metadata) { + break + } + + const deprecation = getDeprecation(metadata) + + if (deprecation) { + context.report({ + node: node.property, + ...message(deprecation), + }) + } + + // stash and pop + namepath.push(node.property.name) + namespace = metadata.namespace + node = node.parent + } + }, + } + }, +}) diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.ts similarity index 78% rename from src/rules/no-duplicates.js rename to src/rules/no-duplicates.ts index c24768277..8d2c5806d 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.ts @@ -1,40 +1,65 @@ -import { resolve } from '../utils/resolve' +import type { TSESLint, TSESTree } from '@typescript-eslint/utils' import semver from 'semver' +import type { PackageJson } from 'type-fest' + +import { createRule } from '../utils' +import { resolve } from '../utils/resolve' +import type { RuleContext } from '../types' -import { docsUrl } from '../docs-url' +let typescriptPkg: PackageJson | undefined -let typescriptPkg try { - typescriptPkg = require('typescript/package.json') // eslint-disable-line import-x/no-extraneous-dependencies -} catch (e) { - /**/ + // eslint-disable-next-line import-x/no-extraneous-dependencies + typescriptPkg = require('typescript/package.json') as PackageJson +} catch { + // } -function checkImports(imported, context) { +type Options = { + considerQueryString?: boolean + 'prefer-inline'?: boolean +} + +type MessageId = 'duplicate' + +function checkImports( + imported: Map, + context: RuleContext, +) { for (const [module, nodes] of imported.entries()) { if (nodes.length > 1) { - const message = `'${module}' imported multiple times.` const [first, ...rest] = nodes const sourceCode = context.getSourceCode() const fix = getFix(first, rest, sourceCode, context) context.report({ node: first.source, - message, + messageId: 'duplicate', + data: { + module, + }, fix, // Attach the autofix (if any) to the first import. }) for (const node of rest) { context.report({ node: node.source, - message, + messageId: 'duplicate', + data: { + module, + }, }) } } } } -function getFix(first, rest, sourceCode, context) { +function getFix( + first: TSESTree.ImportDeclaration, + rest: TSESTree.ImportDeclaration[], + sourceCode: TSESLint.SourceCode, + context: RuleContext, +) { // Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports // requires multiple `fixer.whatever()` calls in the `fix`: We both need to // update the first one, and remove the rest. Support for multiple @@ -54,7 +79,7 @@ function getFix(first, rest, sourceCode, context) { } const defaultImportNames = new Set( - [].concat(first, rest || []).flatMap(x => getDefaultImportName(x) || []), + [first, ...rest].flatMap(x => getDefaultImportName(x) || []), ) // Bail if there are multiple different default import names – it's up to the @@ -93,7 +118,9 @@ function getFix(first, rest, sourceCode, context) { node => !hasSpecifiers(node) && !hasNamespace(node) && - !specifiers.some(specifier => specifier.importNode === node), + !specifiers.some( + specifier => 'importNode' in specifier && specifier.importNode === node, + ), ) const shouldAddDefault = @@ -105,16 +132,16 @@ function getFix(first, rest, sourceCode, context) { return undefined } - return fixer => { + return (fixer: TSESLint.RuleFixer) => { const tokens = sourceCode.getTokens(first) - const openBrace = tokens.find(token => isPunctuator(token, '{')) - const closeBrace = tokens.find(token => isPunctuator(token, '}')) - const firstToken = sourceCode.getFirstToken(first) + const openBrace = tokens.find(token => isPunctuator(token, '{'))! + const closeBrace = tokens.find(token => isPunctuator(token, '}'))! + const firstToken = sourceCode.getFirstToken(first)! const [defaultImportName] = defaultImportNames const firstHasTrailingComma = closeBrace != null && - isPunctuator(sourceCode.getTokenBefore(closeBrace), ',') + isPunctuator(sourceCode.getTokenBefore(closeBrace)!, ',') const firstIsEmpty = !hasSpecifiers(first) const firstExistingIdentifiers = firstIsEmpty ? new Set() @@ -127,14 +154,17 @@ function getFix(first, rest, sourceCode, context) { const [specifiersText] = specifiers.reduce( ([result, needsComma, existingIdentifiers], specifier) => { - const isTypeSpecifier = specifier.importNode.importKind === 'type' + const isTypeSpecifier = + 'importNode' in specifier && + specifier.importNode.importKind === 'type' const preferInline = context.options[0] && context.options[0]['prefer-inline'] // a user might set prefer-inline but not have a supporting TypeScript version. Flow does not support inline types so this should fail in that case as well. if ( preferInline && - (!typescriptPkg || !semver.satisfies(typescriptPkg.version, '>= 4.5')) + (!typescriptPkg || + !semver.satisfies(typescriptPkg.version!, '>= 4.5')) ) { throw new Error( 'Your version of TypeScript does not support inline type imports.', @@ -219,7 +249,7 @@ function getFix(first, rest, sourceCode, context) { const charAfterImportRange = [ importNode.range[1], importNode.range[1] + 1, - ] + ] as const const charAfterImport = sourceCode.text.substring( charAfterImportRange[0], charAfterImportRange[1], @@ -235,7 +265,7 @@ function getFix(first, rest, sourceCode, context) { for (const node of unnecessaryImports) { fixes.push(fixer.remove(node)) - const charAfterImportRange = [node.range[1], node.range[1] + 1] + const charAfterImportRange = [node.range[1], node.range[1] + 1] as const const charAfterImport = sourceCode.text.substring( charAfterImportRange[0], charAfterImportRange[1], @@ -249,12 +279,12 @@ function getFix(first, rest, sourceCode, context) { } } -function isPunctuator(node, value) { +function isPunctuator(node: TSESTree.Token, value: '{' | '}' | ',') { return node.type === 'Punctuator' && node.value === value } // Get the name of the default import of `node`, if any. -function getDefaultImportName(node) { +function getDefaultImportName(node: TSESTree.ImportDeclaration) { const defaultSpecifier = node.specifiers.find( specifier => specifier.type === 'ImportDefaultSpecifier', ) @@ -262,7 +292,7 @@ function getDefaultImportName(node) { } // Checks whether `node` has a namespace import. -function hasNamespace(node) { +function hasNamespace(node: TSESTree.ImportDeclaration) { const specifiers = node.specifiers.filter( specifier => specifier.type === 'ImportNamespaceSpecifier', ) @@ -270,7 +300,7 @@ function hasNamespace(node) { } // Checks whether `node` has any non-default specifiers. -function hasSpecifiers(node) { +function hasSpecifiers(node: TSESTree.ImportDeclaration) { const specifiers = node.specifiers.filter( specifier => specifier.type === 'ImportSpecifier', ) @@ -279,7 +309,10 @@ function hasSpecifiers(node) { // It's not obvious what the user wants to do with comments associated with // duplicate imports, so skip imports with comments when autofixing. -function hasProblematicComments(node, sourceCode) { +function hasProblematicComments( + node: TSESTree.ImportDeclaration, + sourceCode: TSESLint.SourceCode, +) { return ( hasCommentBefore(node, sourceCode) || hasCommentAfter(node, sourceCode) || @@ -289,7 +322,10 @@ function hasProblematicComments(node, sourceCode) { // Checks whether `node` has a comment (that ends) on the previous line or on // the same line as `node` (starts). -function hasCommentBefore(node, sourceCode) { +function hasCommentBefore( + node: TSESTree.ImportDeclaration, + sourceCode: TSESLint.SourceCode, +) { return sourceCode .getCommentsBefore(node) .some(comment => comment.loc.end.line >= node.loc.start.line - 1) @@ -297,7 +333,10 @@ function hasCommentBefore(node, sourceCode) { // Checks whether `node` has a comment (that starts) on the same line as `node` // (ends). -function hasCommentAfter(node, sourceCode) { +function hasCommentAfter( + node: TSESTree.ImportDeclaration, + sourceCode: TSESLint.SourceCode, +) { return sourceCode .getCommentsAfter(node) .some(comment => comment.loc.start.line === node.loc.end.line) @@ -305,7 +344,10 @@ function hasCommentAfter(node, sourceCode) { // Checks whether `node` has any comments _inside,_ except inside the `{...}` // part (if any). -function hasCommentInsideNonSpecifiers(node, sourceCode) { +function hasCommentInsideNonSpecifiers( + node: TSESTree.ImportDeclaration, + sourceCode: TSESLint.SourceCode, +) { const tokens = sourceCode.getTokens(node) const openBraceIndex = tokens.findIndex(token => isPunctuator(token, '{')) const closeBraceIndex = tokens.findIndex(token => isPunctuator(token, '}')) @@ -323,14 +365,14 @@ function hasCommentInsideNonSpecifiers(node, sourceCode) { ) } -module.exports = { +export = createRule<[Options?], MessageId>({ + name: 'no-duplicates', meta: { type: 'problem', docs: { category: 'Style guide', description: 'Forbid repeated import of the same module in multiple places.', - url: docsUrl('no-duplicates'), }, fixable: 'code', schema: [ @@ -347,16 +389,18 @@ module.exports = { additionalProperties: false, }, ], + messages: { + duplicate: "'{{module}}' imported multiple times.", + }, }, - + defaultOptions: [], create(context) { // Prepare the resolver from options. - const considerQueryStringOption = - context.options[0] && context.options[0].considerQueryString - const defaultResolver = sourcePath => + const considerQueryStringOption = context.options[0]?.considerQueryString + const defaultResolver = (sourcePath: string) => resolve(sourcePath, context) || sourcePath const resolver = considerQueryStringOption - ? sourcePath => { + ? (sourcePath: string) => { const parts = sourcePath.match(/^([^?]*)\?(.*)$/) if (!parts) { return defaultResolver(sourcePath) @@ -365,20 +409,28 @@ module.exports = { } : defaultResolver - const moduleMaps = new Map() + const moduleMaps = new Map< + TSESTree.Node, + { + imported: Map + nsImported: Map + defaultTypesImported: Map + namedTypesImported: Map + } + >() - function getImportMap(n) { - if (!moduleMaps.has(n.parent)) { - moduleMaps.set(n.parent, { + function getImportMap(n: TSESTree.ImportDeclaration) { + const parent = n.parent! + if (!moduleMaps.has(parent)) { + moduleMaps.set(parent, { imported: new Map(), nsImported: new Map(), defaultTypesImported: new Map(), namedTypesImported: new Map(), }) } - const map = moduleMaps.get(n.parent) - const preferInline = - context.options[0] && context.options[0]['prefer-inline'] + const map = moduleMaps.get(parent)! + const preferInline = context.options[0]?.['prefer-inline'] if (!preferInline && n.importKind === 'type') { return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' @@ -387,7 +439,9 @@ module.exports = { } if ( !preferInline && - n.specifiers.some(spec => spec.importKind === 'type') + n.specifiers.some( + spec => 'importKind' in spec && spec.importKind === 'type', + ) ) { return map.namedTypesImported } @@ -402,7 +456,7 @@ module.exports = { const importMap = getImportMap(n) if (importMap.has(resolvedPath)) { - importMap.get(resolvedPath).push(n) + importMap.get(resolvedPath)!.push(n) } else { importMap.set(resolvedPath, [n]) } @@ -418,4 +472,4 @@ module.exports = { }, } }, -} +}) diff --git a/src/rules/no-dynamic-require.js b/src/rules/no-dynamic-require.ts similarity index 55% rename from src/rules/no-dynamic-require.js rename to src/rules/no-dynamic-require.ts index 229366455..3a868af47 100644 --- a/src/rules/no-dynamic-require.js +++ b/src/rules/no-dynamic-require.ts @@ -1,6 +1,8 @@ -import { docsUrl } from '../docs-url' +import { TSESTree } from '@typescript-eslint/utils' -function isRequire(node) { +import { createRule } from '../utils' + +function isRequire(node: TSESTree.CallExpression) { return ( node && node.callee && @@ -10,26 +12,37 @@ function isRequire(node) { ) } -function isDynamicImport(node) { - return node && node.callee && node.callee.type === 'Import' +function isDynamicImport(node: TSESTree.CallExpression) { + return ( + node && + node.callee && + // @ts-expect-error - legacy parser type + node.callee.type === 'Import' + ) } -function isStaticValue(arg) { +function isStaticValue( + node: TSESTree.Node, +): node is TSESTree.Literal | TSESTree.TemplateLiteral { return ( - arg.type === 'Literal' || - (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) + node.type === 'Literal' || + (node.type === 'TemplateLiteral' && node.expressions.length === 0) ) } -const dynamicImportErrorMessage = 'Calls to import() should use string literals' +type Options = { + esmodule?: boolean +} + +type MessageId = 'import' | 'require' -module.exports = { +export = createRule<[Options?], MessageId>({ + name: 'no-dynamic-require', meta: { type: 'suggestion', docs: { category: 'Static analysis', description: 'Forbid `require()` calls with expressions.', - url: docsUrl('no-dynamic-require'), }, schema: [ { @@ -42,8 +55,12 @@ module.exports = { additionalProperties: false, }, ], + messages: { + import: 'Calls to import() should use string literals', + require: 'Calls to require() should use string literals', + }, }, - + defaultOptions: [], create(context) { const options = context.options[0] || {} @@ -55,13 +72,13 @@ module.exports = { if (isRequire(node)) { return context.report({ node, - message: 'Calls to require() should use string literals', + messageId: 'require', }) } if (options.esmodule && isDynamicImport(node)) { return context.report({ node, - message: dynamicImportErrorMessage, + messageId: 'import', }) } }, @@ -71,9 +88,9 @@ module.exports = { } return context.report({ node, - message: dynamicImportErrorMessage, + messageId: 'import', }) }, } }, -} +}) diff --git a/src/rules/no-empty-named-blocks.js b/src/rules/no-empty-named-blocks.ts similarity index 61% rename from src/rules/no-empty-named-blocks.js rename to src/rules/no-empty-named-blocks.ts index 619adb1b8..a4ad6d2c2 100644 --- a/src/rules/no-empty-named-blocks.js +++ b/src/rules/no-empty-named-blocks.ts @@ -1,6 +1,8 @@ -import { docsUrl } from '../docs-url' +import type { TSESTree } from '@typescript-eslint/utils' -function getEmptyBlockRange(tokens, index) { +import { createRule } from '../utils' + +function getEmptyBlockRange(tokens: TSESTree.Token[], index: number) { const token = tokens[index] const nextToken = tokens[index + 1] const prevToken = tokens[index - 1] @@ -16,24 +18,29 @@ function getEmptyBlockRange(tokens, index) { start = prevToken.range[0] } - return [start, end] + return [start, end] as const } -module.exports = { +export = createRule({ + name: 'no-empty-named-blocks', meta: { type: 'suggestion', docs: { category: 'Helpful warnings', description: 'Forbid empty named import blocks.', - url: docsUrl('no-empty-named-blocks'), }, fixable: 'code', schema: [], hasSuggestions: true, + messages: { + emptyNamed: 'Unexpected empty named import block', + unused: 'Remove unused import', + emptyImport: 'Remove empty import block', + }, }, - + defaultOptions: [], create(context) { - const importsWithoutNameds = [] + const importsWithoutNameds: TSESTree.ImportDeclaration[] = [] return { ImportDeclaration(node) { @@ -42,18 +49,23 @@ module.exports = { } }, - 'Program:exit'(program) { - const importsTokens = importsWithoutNameds.map(node => [ - node, - program.tokens.filter( - x => x.range[0] >= node.range[0] && x.range[1] <= node.range[1], - ), - ]) + 'Program:exit'(program: TSESTree.Program) { + const importsTokens = importsWithoutNameds.map( + node => + [ + node, + program.tokens!.filter( + x => x.range[0] >= node.range[0] && x.range[1] <= node.range[1], + ), + ] as const, + ) + + const pTokens = program.tokens || [] importsTokens.forEach(([node, tokens]) => { tokens.forEach(token => { - const idx = program.tokens.indexOf(token) - const nextToken = program.tokens[idx + 1] + const idx = pTokens.indexOf(token) + const nextToken = pTokens[idx + 1] if (nextToken && token.value === '{' && nextToken.value === '}') { const hasOtherIdentifiers = tokens.some( @@ -69,39 +81,40 @@ module.exports = { if (!hasOtherIdentifiers) { context.report({ node, - message: 'Unexpected empty named import block', + messageId: 'emptyNamed', suggest: [ { - desc: 'Remove unused import', + messageId: 'unused', fix(fixer) { // Remove the whole import return fixer.remove(node) }, }, { - desc: 'Remove empty import block', + messageId: 'emptyImport', fix(fixer) { // Remove the empty block and the 'from' token, leaving the import only for its side // effects, e.g. `import 'mod'` const sourceCode = context.getSourceCode() - const fromToken = program.tokens.find( - t => t.value === 'from', - ) - const importToken = program.tokens.find( + const fromToken = pTokens.find(t => t.value === 'from')! + const importToken = pTokens.find( t => t.value === 'import', - ) - const hasSpaceAfterFrom = sourceCode.isSpaceBetween( + )! + const hasSpaceAfterFrom = sourceCode.isSpaceBetween!( fromToken, - sourceCode.getTokenAfter(fromToken), + sourceCode.getTokenAfter(fromToken)!, ) - const hasSpaceAfterImport = sourceCode.isSpaceBetween( + const hasSpaceAfterImport = sourceCode.isSpaceBetween!( importToken, - sourceCode.getTokenAfter(fromToken), + sourceCode.getTokenAfter(fromToken)!, ) - const [start] = getEmptyBlockRange(program.tokens, idx) + const [start] = getEmptyBlockRange(pTokens, idx) const [, end] = fromToken.range - const range = [start, hasSpaceAfterFrom ? end + 1 : end] + const range = [ + start, + hasSpaceAfterFrom ? end + 1 : end, + ] as const return fixer.replaceTextRange( range, @@ -114,11 +127,9 @@ module.exports = { } else { context.report({ node, - message: 'Unexpected empty named import block', + messageId: 'emptyNamed', fix(fixer) { - return fixer.removeRange( - getEmptyBlockRange(program.tokens, idx), - ) + return fixer.removeRange(getEmptyBlockRange(pTokens, idx)) }, }) } @@ -128,4 +139,4 @@ module.exports = { }, } }, -} +}) diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.ts similarity index 61% rename from src/rules/no-extraneous-dependencies.js rename to src/rules/no-extraneous-dependencies.ts index a14c8abbd..c28d6ad91 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.ts @@ -1,28 +1,35 @@ import path from 'path' import fs from 'fs' + +import type { TSESTree } from '@typescript-eslint/utils' +import type { PackageJson } from 'type-fest' + import { pkgUp } from '../utils/pkg-up' import { minimatch } from 'minimatch' import { resolve } from '../utils/resolve' import { moduleVisitor } from '../utils/module-visitor' import { importType } from '../core/import-type' import { getFilePackageName } from '../core/package-path' -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' +import type { RuleContext } from '../types' -const depFieldCache = new Map() +type PackageDeps = ReturnType -function hasKeys(obj = {}) { +const depFieldCache = new Map() + +function hasKeys(obj: object = {}) { return Object.keys(obj).length > 0 } -function arrayOrKeys(arrayOrObject) { +function arrayOrKeys(arrayOrObject: object | string[]) { return Array.isArray(arrayOrObject) - ? arrayOrObject + ? (arrayOrObject as string[]) : Object.keys(arrayOrObject) } -function readJSON(jsonPath, throwException) { +function readJSON(jsonPath: string, throwException: boolean) { try { - return JSON.parse(fs.readFileSync(jsonPath, 'utf8')) + return JSON.parse(fs.readFileSync(jsonPath, 'utf8')) as T } catch (err) { if (throwException) { throw err @@ -30,7 +37,7 @@ function readJSON(jsonPath, throwException) { } } -function extractDepFields(pkg) { +function extractDepFields(pkg: PackageJson) { return { dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {}, @@ -44,19 +51,19 @@ function extractDepFields(pkg) { } } -function getPackageDepFields(packageJsonPath, throwAtRead) { +function getPackageDepFields(packageJsonPath: string, throwAtRead: boolean) { if (!depFieldCache.has(packageJsonPath)) { - const depFields = extractDepFields(readJSON(packageJsonPath, throwAtRead)) + const depFields = extractDepFields(readJSON(packageJsonPath, throwAtRead)!) depFieldCache.set(packageJsonPath, depFields) } - return depFieldCache.get(packageJsonPath) } -function getDependencies(context, packageDir) { - let paths = [] +function getDependencies(context: RuleContext, packageDir?: string | string[]) { + let paths: string[] = [] + try { - const packageContent = { + let packageContent: PackageDeps = { dependencies: {}, devDependencies: {}, optionalDependencies: {}, @@ -65,10 +72,10 @@ function getDependencies(context, packageDir) { } if (packageDir && packageDir.length > 0) { - if (!Array.isArray(packageDir)) { - paths = [path.resolve(packageDir)] - } else { + if (Array.isArray(packageDir)) { paths = packageDir.map(dir => path.resolve(dir)) + } else { + paths = [path.resolve(packageDir)] } } @@ -76,21 +83,25 @@ function getDependencies(context, packageDir) { // use rule config to find package.json paths.forEach(dir => { const packageJsonPath = path.join(dir, 'package.json') - const _packageContent = getPackageDepFields(packageJsonPath, true) + const packageContent_ = getPackageDepFields(packageJsonPath, true)! Object.keys(packageContent).forEach(depsKey => { - Object.assign(packageContent[depsKey], _packageContent[depsKey]) + const key = depsKey as keyof PackageDeps + Object.assign(packageContent[key], packageContent_[key]) }) }) } else { + // use closest package.json const packageJsonPath = pkgUp({ cwd: context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), - normalize: false, - }) + })! - // use closest package.json - Object.assign(packageContent, getPackageDepFields(packageJsonPath, false)) + const packageContent_ = getPackageDepFields(packageJsonPath, false) + + if (packageContent_) { + packageContent = packageContent_ + } } if ( @@ -102,50 +113,47 @@ function getDependencies(context, packageDir) { packageContent.bundledDependencies, ].some(hasKeys) ) { - return null + return } return packageContent - } catch (e) { - if (paths.length > 0 && e.code === 'ENOENT') { + } catch (err) { + const error = err as Error & { code: string } + + if (paths.length > 0 && error.code === 'ENOENT') { context.report({ - message: 'The package.json file could not be found.', + messageId: 'pkgNotFound', loc: { line: 0, column: 0 }, }) } - if (e.name === 'JSONError' || e instanceof SyntaxError) { + if (error.name === 'JSONError' || error instanceof SyntaxError) { context.report({ - message: `The package.json file could not be parsed: ${e.message}`, + messageId: 'pkgUnparsable', + data: { error: error.message }, loc: { line: 0, column: 0 }, }) } - - return null } } -function missingErrorMessage(packageName) { - return `'${packageName}' should be listed in the project's dependencies. Run 'npm i -S ${packageName}' to add it` -} - -function devDepErrorMessage(packageName) { - return `'${packageName}' should be listed in the project's dependencies, not devDependencies.` -} - -function optDepErrorMessage(packageName) { - return `'${packageName}' should be listed in the project's dependencies, not optionalDependencies.` -} - -function getModuleOriginalName(name) { +function getModuleOriginalName(name: string) { const [first, second] = name.split('/') return first.startsWith('@') ? `${first}/${second}` : first } -function getModuleRealName(resolved) { - return getFilePackageName(resolved) +type DepDeclaration = { + isInDeps: boolean + isInDevDeps: boolean + isInOptDeps: boolean + isInPeerDeps: boolean + isInBundledDeps: boolean } -function checkDependencyDeclaration(deps, packageName, declarationStatus) { +function checkDependencyDeclaration( + deps: PackageDeps, + packageName: string, + declarationStatus?: DepDeclaration, +) { const newDeclarationStatus = declarationStatus || { isInDeps: false, isInDevDeps: false, @@ -156,8 +164,9 @@ function checkDependencyDeclaration(deps, packageName, declarationStatus) { // in case of sub package.json inside a module // check the dependencies on all hierarchy - const packageHierarchy = [] + const packageHierarchy: string[] = [] const packageNameParts = packageName ? packageName.split('/') : [] + packageNameParts.forEach((namePart, index) => { if (!namePart.startsWith('@')) { const ancestor = packageNameParts.slice(0, index + 1).join('/') @@ -185,19 +194,43 @@ function checkDependencyDeclaration(deps, packageName, declarationStatus) { ) } -function reportIfMissing(context, deps, depsOptions, node, name) { +type DepsOptions = { + allowDevDeps: boolean + allowOptDeps: boolean + allowPeerDeps: boolean + allowBundledDeps: boolean + verifyInternalDeps: boolean + verifyTypeImports: boolean +} + +function reportIfMissing( + context: RuleContext, + deps: PackageDeps, + depsOptions: DepsOptions, + node: TSESTree.Node, + name: string, +) { // Do not report when importing types unless option is enabled if ( !depsOptions.verifyTypeImports && - (node.importKind === 'type' || - node.importKind === 'typeof' || - node.exportKind === 'type' || - (Array.isArray(node.specifiers) && - node.specifiers.length && - node.specifiers.every( + (('importKind' in node && + (node.importKind === 'type' || + // @ts-expect-error - flow type + node.importKind === 'typeof')) || + ('exportKind' in node && node.exportKind === 'type') || + ('specifiers' in node && + Array.isArray(node.specifiers) && + !!node.specifiers.length && + ( + node.specifiers as Array< + TSESTree.ExportSpecifier | TSESTree.ImportClause + > + ).every( specifier => - specifier.importKind === 'type' || - specifier.importKind === 'typeof', + 'importKind' in specifier && + (specifier.importKind === 'type' || + // @ts-expect-error - flow type + specifier.importKind === 'typeof'), ))) ) { return @@ -232,7 +265,7 @@ function reportIfMissing(context, deps, depsOptions, node, name) { // test the real name from the resolved package.json // if not aliased imports (alias/react for example), importPackageName can be misinterpreted - const realPackageName = getModuleRealName(resolved) + const realPackageName = getFilePackageName(resolved) if (realPackageName && realPackageName !== importPackageName) { declarationStatus = checkDependencyDeclaration( deps, @@ -251,50 +284,75 @@ function reportIfMissing(context, deps, depsOptions, node, name) { } } + const packageName = realPackageName || importPackageName + if (declarationStatus.isInDevDeps && !depsOptions.allowDevDeps) { - context.report( + context.report({ node, - devDepErrorMessage(realPackageName || importPackageName), - ) + messageId: 'devDep', + data: { + packageName, + }, + }) return } if (declarationStatus.isInOptDeps && !depsOptions.allowOptDeps) { - context.report( + context.report({ node, - optDepErrorMessage(realPackageName || importPackageName), - ) + messageId: 'optDep', + data: { + packageName, + }, + }) return } - context.report( + context.report({ node, - missingErrorMessage(realPackageName || importPackageName), - ) + messageId: 'missing', + data: { + packageName, + }, + }) } -function testConfig(config, filename) { +function testConfig(config: string[] | boolean | undefined, filename: string) { // Simplest configuration first, either a boolean or nothing. if (typeof config === 'boolean' || typeof config === 'undefined') { return config } // Array of globs. return config.some( - c => - minimatch(filename, c) || - minimatch(filename, path.join(process.cwd(), c)), + c => minimatch(filename, c) || minimatch(filename, path.resolve(c)), ) } -module.exports = { +type Options = { + packageDir?: string | string[] + devDependencies?: boolean + optionalDependencies?: boolean + peerDependencies?: boolean + bundledDependencies?: boolean + includeInternal?: boolean + includeTypes?: boolean +} + +type MessageId = + | 'pkgNotFound' + | 'pkgUnparsable' + | 'devDep' + | 'optDep' + | 'missing' + +export = createRule<[Options?], MessageId>({ + name: 'no-extraneous-dependencies', meta: { type: 'problem', docs: { category: 'Helpful warnings', description: 'Forbid the use of extraneous packages.', - url: docsUrl('no-extraneous-dependencies'), }, - schema: [ { type: 'object', @@ -310,13 +368,25 @@ module.exports = { additionalProperties: false, }, ], + messages: { + pkgNotFound: 'The package.json file could not be found.', + pkgUnparsable: 'The package.json file could not be parsed: {{error}}', + devDep: + "'{{packageName}}' should be listed in the project's dependencies, not devDependencies.", + optDep: + "'{{packageName}}' should be listed in the project's dependencies, not optionalDependencies.", + missing: + "'{{packageName}}' should be listed in the project's dependencies. Run 'npm i -S {{packageName}}' to add it", + }, }, - + defaultOptions: [], create(context) { const options = context.options[0] || {} + const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename() + const deps = getDependencies(context, options.packageDir) || extractDepFields({}) @@ -331,15 +401,16 @@ module.exports = { verifyTypeImports: !!options.includeTypes, } - return moduleVisitor( - (source, node) => { - reportIfMissing(context, deps, depsOptions, node, source.value) + return { + ...moduleVisitor( + (source, node) => { + reportIfMissing(context, deps, depsOptions, node, source.value) + }, + { commonjs: true }, + ), + 'Program:exit'() { + depFieldCache.clear() }, - { commonjs: true }, - ) - }, - - 'Program:exit'() { - depFieldCache.clear() + } }, -} +}) diff --git a/src/rules/no-import-module-exports.js b/src/rules/no-import-module-exports.js deleted file mode 100644 index bb464c0ea..000000000 --- a/src/rules/no-import-module-exports.js +++ /dev/null @@ -1,114 +0,0 @@ -import { minimatch } from 'minimatch' -import path from 'path' -import { pkgUp } from '../utils/pkg-up' - -function getEntryPoint(context) { - const pkgPath = pkgUp({ - cwd: context.getPhysicalFilename - ? context.getPhysicalFilename() - : context.getFilename(), - }) - try { - return require.resolve(path.dirname(pkgPath)) - } catch (error) { - // Assume the package has no entrypoint (e.g. CLI packages) - // in which case require.resolve would throw. - return null - } -} - -function findScope(context, identifier) { - const { scopeManager } = context.getSourceCode() - - return ( - scopeManager && - scopeManager.scopes - .slice() - .reverse() - .find(scope => - scope.variables.some(variable => - variable.identifiers.some(node => node.name === identifier), - ), - ) - ) -} - -function findDefinition(objectScope, identifier) { - const variable = objectScope.variables.find( - variable => variable.name === identifier, - ) - return variable.defs.find(def => def.name.name === identifier) -} - -module.exports = { - meta: { - type: 'problem', - docs: { - category: 'Module systems', - description: 'Forbid import statements with CommonJS module.exports.', - recommended: true, - }, - fixable: 'code', - schema: [ - { - type: 'object', - properties: { - exceptions: { type: 'array' }, - }, - additionalProperties: false, - }, - ], - }, - create(context) { - const importDeclarations = [] - const entryPoint = getEntryPoint(context) - const options = context.options[0] || {} - let alreadyReported = false - - function report(node) { - const fileName = context.getPhysicalFilename - ? context.getPhysicalFilename() - : context.getFilename() - const isEntryPoint = entryPoint === fileName - const isIdentifier = node.object.type === 'Identifier' - const hasKeywords = /^(module|exports)$/.test(node.object.name) - const objectScope = hasKeywords && findScope(context, node.object.name) - const variableDefinition = - objectScope && findDefinition(objectScope, node.object.name) - const isImportBinding = - variableDefinition && variableDefinition.type === 'ImportBinding' - const hasCJSExportReference = - hasKeywords && (!objectScope || objectScope.type === 'module') - const isException = - !!options.exceptions && - options.exceptions.some(glob => minimatch(fileName, glob)) - - if ( - isIdentifier && - hasCJSExportReference && - !isEntryPoint && - !isException && - !isImportBinding - ) { - importDeclarations.forEach(importDeclaration => { - context.report({ - node: importDeclaration, - message: `Cannot use import declarations in modules that export using CommonJS (module.exports = 'foo' or exports.bar = 'hi')`, - }) - }) - alreadyReported = true - } - } - - return { - ImportDeclaration(node) { - importDeclarations.push(node) - }, - MemberExpression(node) { - if (!alreadyReported) { - report(node) - } - }, - } - }, -} diff --git a/src/rules/no-import-module-exports.ts b/src/rules/no-import-module-exports.ts new file mode 100644 index 000000000..c68de2db4 --- /dev/null +++ b/src/rules/no-import-module-exports.ts @@ -0,0 +1,135 @@ +import path from 'path' + +import type { TSESLint, TSESTree } from '@typescript-eslint/utils' +import { minimatch } from 'minimatch' + +import type { RuleContext } from '../types' +import { pkgUp } from '../utils/pkg-up' +import { createRule } from '../utils' + +function getEntryPoint(context: RuleContext) { + const pkgPath = pkgUp({ + cwd: context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(), + })! + try { + return require.resolve(path.dirname(pkgPath)) + } catch (error) { + // Assume the package has no entrypoint (e.g. CLI packages) + // in which case require.resolve would throw. + return null + } +} + +function findScope(context: RuleContext, identifier: string) { + const { scopeManager } = context.getSourceCode() + return scopeManager?.scopes + .slice() + .reverse() + .find(scope => + scope.variables.some(variable => + variable.identifiers.some(node => node.name === identifier), + ), + ) +} + +function findDefinition(objectScope: TSESLint.Scope.Scope, identifier: string) { + const variable = objectScope.variables.find( + variable => variable.name === identifier, + )! + return variable.defs.find( + def => 'name' in def.name && def.name.name === identifier, + ) +} + +type Options = { + exceptions?: string[] +} + +type MessageId = 'notAllowed' + +export = createRule<[Options?], MessageId>({ + name: 'no-import-module-exports', + meta: { + type: 'problem', + docs: { + category: 'Module systems', + description: 'Forbid import statements with CommonJS module.exports.', + recommended: true, + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + exceptions: { type: 'array' }, + }, + additionalProperties: false, + }, + ], + messages: { + notAllowed: + "Cannot use import declarations in modules that export using CommonJS (module.exports = 'foo' or exports.bar = 'hi')", + }, + }, + defaultOptions: [], + create(context) { + const importDeclarations: TSESTree.ImportDeclaration[] = [] + const entryPoint = getEntryPoint(context) + const options = context.options[0] || {} + + let alreadyReported = false + + return { + ImportDeclaration(node) { + importDeclarations.push(node) + }, + MemberExpression(node) { + if (alreadyReported) { + return + } + + const fileName = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename() + const isEntryPoint = entryPoint === fileName + const isIdentifier = node.object.type === 'Identifier' + + if (!('name' in node.object)) { + return + } + + const hasKeywords = /^(module|exports)$/.test(node.object.name) + const objectScope = hasKeywords + ? findScope(context, node.object.name) + : undefined + const variableDefinition = + objectScope && findDefinition(objectScope, node.object.name) + + const isImportBinding = variableDefinition?.type === 'ImportBinding' + const hasCJSExportReference = + hasKeywords && (!objectScope || objectScope.type === 'module') + const isException = + !!options.exceptions && + options.exceptions.some(glob => minimatch(fileName, glob)) + + if ( + isIdentifier && + hasCJSExportReference && + !isEntryPoint && + !isException && + !isImportBinding + ) { + importDeclarations.forEach(importDeclaration => { + context.report({ + node: importDeclaration, + messageId: 'notAllowed', + }) + }) + alreadyReported = true + } + }, + } + }, +}) diff --git a/src/rules/no-internal-modules.ts b/src/rules/no-internal-modules.ts index 67d54610a..ce9874175 100644 --- a/src/rules/no-internal-modules.ts +++ b/src/rules/no-internal-modules.ts @@ -85,10 +85,10 @@ export = createRule<[Options?], MessageId>({ const options = context.options[0] || {} const allowRegexps = (options.allow || []) .map(p => makeRe(p)) - .filter(Boolean) as RegExp[] + .filter(Boolean) const forbidRegexps = (options.forbid || []) .map(p => makeRe(p)) - .filter(Boolean) as RegExp[] + .filter(Boolean) // test if reaching to this destination is allowed function reachingAllowed(importPath: string) { diff --git a/src/rules/no-named-export.js b/src/rules/no-named-export.ts similarity index 52% rename from src/rules/no-named-export.js rename to src/rules/no-named-export.ts index 8bef9c85b..3cc2434f1 100644 --- a/src/rules/no-named-export.js +++ b/src/rules/no-named-export.ts @@ -1,42 +1,45 @@ -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' -module.exports = { +export = createRule({ + name: 'no-named-export', meta: { type: 'suggestion', docs: { category: 'Style guide', description: 'Forbid named exports.', - url: docsUrl('no-named-export'), }, schema: [], + messages: { + noAllowed: 'Named exports are not allowed.', + }, }, - + defaultOptions: [], create(context) { // ignore non-modules if (context.parserOptions.sourceType !== 'module') { return {} } - const message = 'Named exports are not allowed.' - return { ExportAllDeclaration(node) { - context.report({ node, message }) + context.report({ node, messageId: 'noAllowed' }) }, ExportNamedDeclaration(node) { if (node.specifiers.length === 0) { - return context.report({ node, message }) + return context.report({ node, messageId: 'noAllowed' }) } const someNamed = node.specifiers.some( specifier => - (specifier.exported.name || specifier.exported.value) !== 'default', + (specifier.exported.name || + ('value' in specifier.exported && specifier.exported.value)) !== + 'default', ) if (someNamed) { - context.report({ node, message }) + context.report({ node, messageId: 'noAllowed' }) } }, } }, -} +}) diff --git a/src/rules/no-nodejs-modules.js b/src/rules/no-nodejs-modules.ts similarity index 54% rename from src/rules/no-nodejs-modules.js rename to src/rules/no-nodejs-modules.ts index bd5f6d5f9..186d7bd94 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.ts @@ -1,20 +1,20 @@ import { importType } from '../core/import-type' import { moduleVisitor } from '../utils/module-visitor' -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' -function reportIfMissing(context, node, allowed, name) { - if (allowed.indexOf(name) === -1 && importType(name, context) === 'builtin') { - context.report(node, `Do not import Node.js builtin module "${name}"`) - } +type Options = { + allow?: string[] } -module.exports = { +type MessageId = 'builtin' + +export = createRule<[Options?], MessageId>({ + name: 'no-nodejs-modules', meta: { type: 'suggestion', docs: { category: 'Module systems', description: 'Forbid Node.js builtin modules.', - url: docsUrl('no-nodejs-modules'), }, schema: [ { @@ -31,17 +31,32 @@ module.exports = { additionalProperties: false, }, ], + messages: { + builtin: 'Do not import Node.js builtin module "{{moduleName}}"', + }, }, - + defaultOptions: [], create(context) { const options = context.options[0] || {} const allowed = options.allow || [] return moduleVisitor( (source, node) => { - reportIfMissing(context, node, allowed, source.value) + const moduleName = source.value + if ( + allowed.indexOf(moduleName) === -1 && + importType(moduleName, context) === 'builtin' + ) { + context.report({ + node, + messageId: 'builtin', + data: { + moduleName, + }, + }) + } }, { commonjs: true }, ) }, -} +}) diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js deleted file mode 100644 index cc81f2f00..000000000 --- a/src/rules/no-unassigned-import.js +++ /dev/null @@ -1,89 +0,0 @@ -import path from 'path' -import { minimatch } from 'minimatch' - -import { isStaticRequire } from '../core/static-require' -import { docsUrl } from '../docs-url' - -function report(context, node) { - context.report({ - node, - message: 'Imported module should be assigned', - }) -} - -function testIsAllow(globs, filename, source) { - if (!Array.isArray(globs)) { - return false // default doesn't allow any patterns - } - - let filePath - - if (source[0] !== '.' && source[0] !== '/') { - // a node module - filePath = source - } else { - filePath = path.resolve(path.dirname(filename), source) // get source absolute path - } - - return ( - globs.find( - glob => - minimatch(filePath, glob) || - minimatch(filePath, path.join(process.cwd(), glob)), - ) !== undefined - ) -} - -function create(context) { - const options = context.options[0] || {} - const filename = context.getPhysicalFilename - ? context.getPhysicalFilename() - : context.getFilename() - const isAllow = source => testIsAllow(options.allow, filename, source) - - return { - ImportDeclaration(node) { - if (node.specifiers.length === 0 && !isAllow(node.source.value)) { - report(context, node) - } - }, - ExpressionStatement(node) { - if ( - node.expression.type === 'CallExpression' && - isStaticRequire(node.expression) && - !isAllow(node.expression.arguments[0].value) - ) { - report(context, node.expression) - } - }, - } -} - -module.exports = { - create, - meta: { - type: 'suggestion', - docs: { - category: 'Style guide', - description: 'Forbid unassigned imports', - url: docsUrl('no-unassigned-import'), - }, - schema: [ - { - type: 'object', - properties: { - devDependencies: { type: ['boolean', 'array'] }, - optionalDependencies: { type: ['boolean', 'array'] }, - peerDependencies: { type: ['boolean', 'array'] }, - allow: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - additionalProperties: false, - }, - ], - }, -} diff --git a/src/rules/no-unassigned-import.ts b/src/rules/no-unassigned-import.ts new file mode 100644 index 000000000..9d95a25a2 --- /dev/null +++ b/src/rules/no-unassigned-import.ts @@ -0,0 +1,105 @@ +import path from 'path' +import { minimatch } from 'minimatch' + +import { isStaticRequire } from '../core/static-require' +import { createRule } from '../utils' + +function testIsAllow( + globs: string[] | undefined, + filename: string, + source: string, +) { + if (!Array.isArray(globs)) { + return false // default doesn't allow any patterns + } + + let filePath: string + + if (source[0] !== '.' && source[0] !== '/') { + // a node module + filePath = source + } else { + filePath = path.resolve(path.dirname(filename), source) // get source absolute path + } + + return ( + globs.find( + glob => + minimatch(filePath, glob) || + minimatch(filePath, path.join(process.cwd(), glob)), + ) !== undefined + ) +} + +type Options = { + allow?: string[] +} + +type MessageId = 'unassigned' + +export = createRule<[Options?], MessageId>({ + name: 'no-unassigned-import', + meta: { + type: 'suggestion', + docs: { + category: 'Style guide', + description: 'Forbid unassigned imports', + }, + schema: [ + { + type: 'object', + properties: { + devDependencies: { type: ['boolean', 'array'] }, + optionalDependencies: { type: ['boolean', 'array'] }, + peerDependencies: { type: ['boolean', 'array'] }, + allow: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], + messages: { + unassigned: 'Imported module should be assigned', + }, + }, + defaultOptions: [], + create(context) { + const options = context.options[0] || {} + + const filename = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename() + + const isAllow = (source: string) => + testIsAllow(options.allow, filename, source) + + return { + ImportDeclaration(node) { + if (node.specifiers.length === 0 && !isAllow(node.source.value)) { + context.report({ + node, + messageId: 'unassigned', + }) + } + }, + ExpressionStatement(node) { + if ( + node.expression.type === 'CallExpression' && + isStaticRequire(node.expression) && + 'value' in node.expression.arguments[0] && + typeof node.expression.arguments[0].value === 'string' && + !isAllow(node.expression.arguments[0].value) + ) { + context.report({ + node: node.expression, + messageId: 'unassigned', + }) + } + }, + } + }, +}) diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.ts similarity index 73% rename from src/rules/no-useless-path-segments.js rename to src/rules/no-useless-path-segments.ts index 3ba7098e3..b8f173739 100644 --- a/src/rules/no-useless-path-segments.js +++ b/src/rules/no-useless-path-segments.ts @@ -1,13 +1,13 @@ /** - * @fileOverview Ensures that there are no useless path segments - * @author Thomas Grainger + * Ensures that there are no useless path segments */ +import path from 'path' + import { getFileExtensions } from '../utils/ignore' -import { moduleVisitor } from '../utils/module-visitor' +import { ModuleOptions, moduleVisitor } from '../utils/module-visitor' import { resolve } from '../utils/resolve' -import path from 'path' -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' /** * convert a potentially relative path from node utils into a true @@ -19,35 +19,39 @@ import { docsUrl } from '../docs-url' * ..foo/bar -> ./..foo/bar * foo/bar -> ./foo/bar * - * @param relativePath {string} relative posix path potentially missing leading './' - * @returns {string} relative posix path that always starts with a ./ + * @param relativePath relative posix path potentially missing leading './' + * @returns relative posix path that always starts with a ./ **/ -function toRelativePath(relativePath) { +function toRelativePath(relativePath: string): string { const stripped = relativePath.replace(/\/$/g, '') // Remove trailing / return /^((\.\.)|(\.))($|\/)/.test(stripped) ? stripped : `./${stripped}` } -function normalize(fn) { - return toRelativePath(path.posix.normalize(fn)) +function normalize(filepath: string) { + return toRelativePath(path.posix.normalize(filepath)) } -function countRelativeParents(pathSegments) { +function countRelativeParents(pathSegments: string[]) { return pathSegments.filter(x => x === '..').length } -module.exports = { +type Options = ModuleOptions & { + noUselessIndex?: boolean +} + +type MessageId = 'useless' + +export = createRule<[Options?], MessageId>({ + name: 'no-useless-path-segments', meta: { type: 'suggestion', docs: { category: 'Static analysis', description: 'Forbid unnecessary path segments in import and require statements.', - url: docsUrl('no-useless-path-segments'), }, - fixable: 'code', - schema: [ { type: 'object', @@ -58,27 +62,36 @@ module.exports = { additionalProperties: false, }, ], + messages: { + useless: + 'Useless path segments for "{{importPath}}", should be "{{proposedPath}}"', + }, }, - + defaultOptions: [], create(context) { const currentDir = path.dirname( context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(), ) - const options = context.options[0] - function checkSourceValue(source) { + const options = context.options[0] || {} + + return moduleVisitor(source => { const { value: importPath } = source - function reportWithProposedPath(proposedPath) { + function reportWithProposedPath(proposedPath: string) { context.report({ node: source, - // Note: Using messageIds is not possible due to the support for ESLint 2 and 3 - message: `Useless path segments for "${importPath}", should be "${proposedPath}"`, + messageId: 'useless', + data: { + importPath, + proposedPath, + }, fix: fixer => - proposedPath && - fixer.replaceText(source, JSON.stringify(proposedPath)), + proposedPath + ? fixer.replaceText(source, JSON.stringify(proposedPath)) + : null, }) } @@ -88,7 +101,7 @@ module.exports = { } // Report rule violation if path is not the shortest possible - const resolvedPath = resolve(importPath, context) + const resolvedPath = resolve(importPath, context)! const normedPath = normalize(importPath) const resolvedNormedPath = resolve(normedPath, context) if (normedPath !== importPath && resolvedPath === resolvedNormedPath) { @@ -101,11 +114,7 @@ module.exports = { ) // Check if path contains unnecessary index (including a configured extension) - if ( - options && - options.noUselessIndex && - regexUnnecessaryIndex.test(importPath) - ) { + if (options.noUselessIndex && regexUnnecessaryIndex.test(importPath)) { const parentDirectory = path.dirname(importPath) // Try to find ambiguous imports @@ -154,8 +163,6 @@ module.exports = { .join('/'), ), ) - } - - return moduleVisitor(checkSourceValue, options) + }, options) }, -} +}) diff --git a/src/rules/no-webpack-loader-syntax.js b/src/rules/no-webpack-loader-syntax.js deleted file mode 100644 index 8891490a7..000000000 --- a/src/rules/no-webpack-loader-syntax.js +++ /dev/null @@ -1,32 +0,0 @@ -import { moduleVisitor } from '../utils/module-visitor' -import { docsUrl } from '../docs-url' - -function reportIfNonStandard(context, node, name) { - if (name && name.indexOf('!') !== -1) { - context.report( - node, - `Unexpected '!' in '${name}'. Do not use import syntax to configure webpack loaders.`, - ) - } -} - -module.exports = { - meta: { - type: 'problem', - docs: { - category: 'Static analysis', - description: 'Forbid webpack loader syntax in imports.', - url: docsUrl('no-webpack-loader-syntax'), - }, - schema: [], - }, - - create(context) { - return moduleVisitor( - (source, node) => { - reportIfNonStandard(context, node, source.value) - }, - { commonjs: true }, - ) - }, -} diff --git a/src/rules/no-webpack-loader-syntax.ts b/src/rules/no-webpack-loader-syntax.ts new file mode 100644 index 000000000..c26c51f3f --- /dev/null +++ b/src/rules/no-webpack-loader-syntax.ts @@ -0,0 +1,35 @@ +import { moduleVisitor } from '../utils/module-visitor' +import { createRule } from '../utils' + +export = createRule({ + name: 'no-webpack-loader-syntax', + meta: { + type: 'problem', + docs: { + category: 'Static analysis', + description: 'Forbid webpack loader syntax in imports.', + }, + schema: [], + messages: { + unexpected: + "Unexpected '!' in '{{name}}'. Do not use import syntax to configure webpack loaders.", + }, + }, + defaultOptions: [], + create(context) { + return moduleVisitor( + (source, node) => { + if (source.value?.includes('!')) { + context.report({ + node, + messageId: 'unexpected', + data: { + name: source.value, + }, + }) + } + }, + { commonjs: true }, + ) + }, +}) diff --git a/src/rules/order.js b/src/rules/order.ts similarity index 62% rename from src/rules/order.js rename to src/rules/order.ts index eee46d7af..fdfcfab67 100644 --- a/src/rules/order.js +++ b/src/rules/order.ts @@ -1,24 +1,43 @@ -'use strict' - -import { minimatch } from 'minimatch' +import type { TSESLint, TSESTree } from '@typescript-eslint/utils' +import { type MinimatchOptions, minimatch } from 'minimatch' import { importType } from '../core/import-type' import { isStaticRequire } from '../core/static-require' -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' +import type { + AlphabetizeOptions, + Arrayable, + ImportType, + PathGroup, + RuleContext, +} from '../types' + +interface ImportEntryWithRank extends ImportEntry { + rank: number +} // This is a **non-spec compliant** but works in practice replacement of `object.groupby` package. -const groupBy = (array, grouper) => - array.reduce((acc, curr, index) => { +const groupBy = ( + array: T[], + grouper: (curr: T, index: number) => string | number, +) => + array.reduce>((acc, curr, index) => { const key = grouper(curr, index) ;(acc[key] ||= []).push(curr) return acc }, {}) -const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index'] +const defaultGroups = [ + 'builtin', + 'external', + 'parent', + 'sibling', + 'index', +] as const // REPORTING AND FIXING -function reverse(array) { +function reverse(array: ImportEntryWithRank[]): ImportEntryWithRank[] { return array .map(function (v) { return { ...v, rank: -v.rank } @@ -26,11 +45,17 @@ function reverse(array) { .reverse() } -function getTokensOrCommentsAfter(sourceCode, node, count) { - let currentNodeOrToken = node - const result = [] +function getTokensOrCommentsAfter( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Node, + count: number, +) { + let currentNodeOrToken: TSESTree.Node | TSESTree.Token | null = node + const result: Array = [] for (let i = 0; i < count; i++) { - currentNodeOrToken = sourceCode.getTokenOrCommentAfter(currentNodeOrToken) + currentNodeOrToken = sourceCode.getTokenAfter(currentNodeOrToken, { + includeComments: true, + }) if (currentNodeOrToken == null) { break } @@ -39,11 +64,17 @@ function getTokensOrCommentsAfter(sourceCode, node, count) { return result } -function getTokensOrCommentsBefore(sourceCode, node, count) { - let currentNodeOrToken = node - const result = [] +function getTokensOrCommentsBefore( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Node, + count: number, +) { + let currentNodeOrToken: TSESTree.Node | TSESTree.Token | null = node + const result: Array = [] for (let i = 0; i < count; i++) { - currentNodeOrToken = sourceCode.getTokenOrCommentBefore(currentNodeOrToken) + currentNodeOrToken = sourceCode.getTokenBefore(currentNodeOrToken, { + includeComments: true, + }) if (currentNodeOrToken == null) { break } @@ -52,9 +83,13 @@ function getTokensOrCommentsBefore(sourceCode, node, count) { return result.reverse() } -function takeTokensAfterWhile(sourceCode, node, condition) { +function takeTokensAfterWhile( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Node, + condition: (nodeOrToken: TSESTree.Node | TSESTree.Token) => boolean, +) { const tokens = getTokensOrCommentsAfter(sourceCode, node, 100) - const result = [] + const result: Array = [] for (let i = 0; i < tokens.length; i++) { if (condition(tokens[i])) { result.push(tokens[i]) @@ -65,9 +100,13 @@ function takeTokensAfterWhile(sourceCode, node, condition) { return result } -function takeTokensBeforeWhile(sourceCode, node, condition) { +function takeTokensBeforeWhile( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Node, + condition: (nodeOrToken: TSESTree.Node | TSESTree.Token) => boolean, +) { const tokens = getTokensOrCommentsBefore(sourceCode, node, 100) - const result = [] + const result: Array = [] for (let i = tokens.length - 1; i >= 0; i--) { if (condition(tokens[i])) { result.push(tokens[i]) @@ -78,7 +117,7 @@ function takeTokensBeforeWhile(sourceCode, node, condition) { return result.reverse() } -function findOutOfOrder(imported) { +function findOutOfOrder(imported: ImportEntryWithRank[]) { if (imported.length === 0) { return [] } @@ -92,15 +131,21 @@ function findOutOfOrder(imported) { }) } -function findRootNode(node) { +function findRootNode(node: TSESTree.Node) { let parent = node - while (parent.parent != null && parent.parent.body == null) { + while ( + parent.parent != null && + (!('body' in parent.parent) || parent.parent.body == null) + ) { parent = parent.parent } return parent } -function findEndOfLineWithComments(sourceCode, node) { +function findEndOfLineWithComments( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Node, +) { const tokensToEndOfLine = takeTokensAfterWhile( sourceCode, node, @@ -128,14 +173,17 @@ function findEndOfLineWithComments(sourceCode, node) { return result } -function commentOnSameLineAs(node) { - return token => +function commentOnSameLineAs(node: TSESTree.Node) { + return (token: TSESTree.Node | TSESTree.Token) => (token.type === 'Block' || token.type === 'Line') && token.loc.start.line === token.loc.end.line && token.loc.end.line === node.loc.end.line } -function findStartOfLineWithComments(sourceCode, node) { +function findStartOfLineWithComments( + sourceCode: TSESLint.SourceCode, + node: TSESTree.Node, +) { const tokensToEndOfLine = takeTokensBeforeWhile( sourceCode, node, @@ -153,11 +201,14 @@ function findStartOfLineWithComments(sourceCode, node) { return result } -function isRequireExpression(expr) { +function isRequireExpression( + expr?: TSESTree.Expression | null, +): expr is TSESTree.CallExpression { return ( expr != null && expr.type === 'CallExpression' && expr.callee != null && + 'name' in expr.callee && expr.callee.name === 'require' && expr.arguments != null && expr.arguments.length === 1 && @@ -165,7 +216,7 @@ function isRequireExpression(expr) { ) } -function isSupportedRequireModule(node) { +function isSupportedRequireModule(node: TSESTree.Node) { if (node.type !== 'VariableDeclaration') { return false } @@ -188,7 +239,9 @@ function isSupportedRequireModule(node) { return isPlainRequire || isRequireWithMemberExpression } -function isPlainImportModule(node) { +function isPlainImportModule( + node: TSESTree.Node, +): node is TSESTree.ImportDeclaration { return ( node.type === 'ImportDeclaration' && node.specifiers != null && @@ -196,13 +249,19 @@ function isPlainImportModule(node) { ) } -function isPlainImportEquals(node) { +function isPlainImportEquals( + node: TSESTree.Node, +): node is TSESTree.TSImportEqualsDeclaration & { + moduleReference: TSESTree.TSExternalModuleReference +} { return ( - node.type === 'TSImportEqualsDeclaration' && node.moduleReference.expression + node.type === 'TSImportEqualsDeclaration' && + 'expression' in node.moduleReference && + !!node.moduleReference.expression ) } -function canCrossNodeWhileReorder(node) { +function canCrossNodeWhileReorder(node: TSESTree.Node) { return ( isSupportedRequireModule(node) || isPlainImportModule(node) || @@ -210,11 +269,11 @@ function canCrossNodeWhileReorder(node) { ) } -function canReorderItems(firstNode, secondNode) { - const parent = firstNode.parent +function canReorderItems(firstNode: TSESTree.Node, secondNode: TSESTree.Node) { + const parent = firstNode.parent as TSESTree.Program const [firstIndex, secondIndex] = [ - parent.body.indexOf(firstNode), - parent.body.indexOf(secondNode), + parent.body.findIndex(it => it === firstNode), + parent.body.findIndex(it => it === secondNode), ].sort() const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1) for (const nodeBetween of nodesBetween) { @@ -225,17 +284,25 @@ function canReorderItems(firstNode, secondNode) { return true } -function makeImportDescription(node) { - if (node.node.importKind === 'type') { - return 'type import' - } - if (node.node.importKind === 'typeof') { - return 'typeof import' +function makeImportDescription(node: ImportEntry) { + if ('importKind' in node.node) { + if (node.node.importKind === 'type') { + return 'type import' + } + // @ts-expect-error - flow type + if (node.node.importKind === 'typeof') { + return 'typeof import' + } } return 'import' } -function fixOutOfOrder(context, firstNode, secondNode, order) { +function fixOutOfOrder( + context: RuleContext, + firstNode: ImportEntryWithRank, + secondNode: ImportEntryWithRank, + order: 'before' | 'after', +) { const sourceCode = context.getSourceCode() const firstRoot = findRootNode(firstNode.node) @@ -254,46 +321,52 @@ function fixOutOfOrder(context, firstNode, secondNode, order) { const firstImport = `${makeImportDescription(firstNode)} of \`${firstNode.displayName}\`` const secondImport = `\`${secondNode.displayName}\` ${makeImportDescription(secondNode)}` - const message = `${secondImport} should occur ${order} ${firstImport}` - - if (order === 'before') { - context.report({ - node: secondNode.node, - message, - fix: - canFix && - (fixer => - fixer.replaceTextRange( - [firstRootStart, secondRootEnd], - newCode + - sourceCode.text.substring(firstRootStart, secondRootStart), - )), - }) - } else if (order === 'after') { - context.report({ - node: secondNode.node, - message, - fix: - canFix && - (fixer => - fixer.replaceTextRange( - [secondRootStart, firstRootEnd], - sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode, - )), - }) - } + + context.report({ + node: secondNode.node, + messageId: 'order', + data: { + firstImport, + secondImport, + order, + }, + fix: canFix + ? fixer => + order === 'before' + ? fixer.replaceTextRange( + [firstRootStart, secondRootEnd], + newCode + + sourceCode.text.substring(firstRootStart, secondRootStart), + ) + : fixer.replaceTextRange( + [secondRootStart, firstRootEnd], + sourceCode.text.substring(secondRootEnd, firstRootEnd) + + newCode, + ) + : null, + }) } -function reportOutOfOrder(context, imported, outOfOrder, order) { - outOfOrder.forEach(function (imp) { - const found = imported.find(function hasHigherRank(importedItem) { - return importedItem.rank > imp.rank - }) - fixOutOfOrder(context, found, imp, order) +function reportOutOfOrder( + context: RuleContext, + imported: ImportEntryWithRank[], + outOfOrder: ImportEntryWithRank[], + order: 'before' | 'after', +) { + outOfOrder.forEach(imp => { + fixOutOfOrder( + context, + imported.find(importedItem => importedItem.rank > imp.rank)!, + imp, + order, + ) }) } -function makeOutOfOrderReport(context, imported) { +function makeOutOfOrderReport( + context: RuleContext, + imported: ImportEntryWithRank[], +) { const outOfOrder = findOutOfOrder(imported) if (!outOfOrder.length) { return @@ -309,7 +382,7 @@ function makeOutOfOrderReport(context, imported) { reportOutOfOrder(context, imported, outOfOrder, 'before') } -const compareString = (a, b) => { +const compareString = (a: string, b: string) => { if (a < b) { return -1 } @@ -320,20 +393,21 @@ const compareString = (a, b) => { } /** Some parsers (languages without types) don't provide ImportKind */ -const DEAFULT_IMPORT_KIND = 'value' -const getNormalizedValue = (node, toLowerCase) => { +const DEFAULT_IMPORT_KIND = 'value' + +const getNormalizedValue = (node: ImportEntry, toLowerCase?: boolean) => { const value = node.value return toLowerCase ? String(value).toLowerCase() : value } -function getSorter(alphabetizeOptions) { +function getSorter(alphabetizeOptions: AlphabetizeOptions) { const multiplier = alphabetizeOptions.order === 'asc' ? 1 : -1 const orderImportKind = alphabetizeOptions.orderImportKind const multiplierImportKind = orderImportKind !== 'ignore' && (alphabetizeOptions.orderImportKind === 'asc' ? 1 : -1) - return function importsSorter(nodeA, nodeB) { + return (nodeA: ImportEntry, nodeB: ImportEntry) => { const importA = getNormalizedValue( nodeA, alphabetizeOptions.caseInsensitive, @@ -371,8 +445,10 @@ function getSorter(alphabetizeOptions) { result = multiplierImportKind * compareString( - nodeA.node.importKind || DEAFULT_IMPORT_KIND, - nodeB.node.importKind || DEAFULT_IMPORT_KIND, + ('importKind' in nodeA.node && nodeA.node.importKind) || + DEFAULT_IMPORT_KIND, + ('importKind' in nodeB.node && nodeB.node.importKind) || + DEFAULT_IMPORT_KIND, ) } @@ -380,42 +456,66 @@ function getSorter(alphabetizeOptions) { } } -function mutateRanksToAlphabetize(imported, alphabetizeOptions) { +function mutateRanksToAlphabetize( + imported: ImportEntryWithRank[], + alphabetizeOptions: AlphabetizeOptions, +) { const groupedByRanks = groupBy(imported, item => item.rank) const sorterFn = getSorter(alphabetizeOptions) // sort group keys so that they can be iterated on in order - const groupRanks = Object.keys(groupedByRanks).sort(function (a, b) { - return a - b - }) + const groupRanks = Object.keys(groupedByRanks).sort((a, b) => +a - +b) // sort imports locally within their group - groupRanks.forEach(function (groupRank) { + groupRanks.forEach(groupRank => { groupedByRanks[groupRank].sort(sorterFn) }) // assign globally unique rank to each import let newRank = 0 - const alphabetizedRanks = groupRanks.reduce(function (acc, groupRank) { - groupedByRanks[groupRank].forEach(function (importedItem) { - acc[`${importedItem.value}|${importedItem.node.importKind}`] = - parseInt(groupRank, 10) + newRank - newRank += 1 - }) - return acc - }, {}) + const alphabetizedRanks = groupRanks.reduce>( + (acc, groupRank) => { + groupedByRanks[groupRank].forEach(importedItem => { + acc[ + `${importedItem.value}|${'importKind' in importedItem.node ? importedItem.node.importKind : ''}` + ] = parseInt(groupRank, 10) + newRank + newRank += 1 + }) + return acc + }, + {}, + ) // mutate the original group-rank with alphabetized-rank - imported.forEach(function (importedItem) { + imported.forEach(importedItem => { importedItem.rank = - alphabetizedRanks[`${importedItem.value}|${importedItem.node.importKind}`] + alphabetizedRanks[ + `${importedItem.value}|${'importKind' in importedItem.node ? importedItem.node.importKind : ''}` + ] }) } +type Ranks = { + omittedTypes: string[] + groups: Record + pathGroups: Array<{ + pattern: string + patternOptions?: MinimatchOptions + group: string + position?: number + }> + maxPosition: number +} + // DETECTING -function computePathRank(ranks, pathGroups, path, maxPosition) { +function computePathRank( + ranks: Ranks['groups'], + pathGroups: Ranks['pathGroups'], + path: string, + maxPosition: number, +) { for (let i = 0, l = pathGroups.length; i < l; i++) { const { pattern, patternOptions, group, position = 1 } = pathGroups[i] if (minimatch(path, pattern, patternOptions || { nocomment: true })) { @@ -424,14 +524,27 @@ function computePathRank(ranks, pathGroups, path, maxPosition) { } } -function computeRank(context, ranks, importEntry, excludedImportTypes) { - let impType +type ImportEntry = { + type: 'import:object' | 'import' | 'require' + node: TSESTree.Node + value: string + displayName: string +} + +function computeRank( + context: RuleContext, + ranks: Ranks, + importEntry: ImportEntry, + excludedImportTypes: Set, +) { + let impType: ImportType let rank if (importEntry.type === 'import:object') { impType = 'object' } else if ( + 'importKind' in importEntry.node && importEntry.node.importKind === 'type' && - ranks.omittedTypes.indexOf('type') === -1 + !ranks.omittedTypes.includes('type') ) { impType = 'type' } else { @@ -459,11 +572,11 @@ function computeRank(context, ranks, importEntry, excludedImportTypes) { } function registerNode( - context, - importEntry, - ranks, - imported, - excludedImportTypes, + context: RuleContext, + importEntry: ImportEntry, + ranks: Ranks, + imported: ImportEntryWithRank[], + excludedImportTypes: Set, ) { const rank = computeRank(context, ranks, importEntry, excludedImportTypes) if (rank !== -1) { @@ -471,20 +584,21 @@ function registerNode( } } -function getRequireBlock(node) { +function getRequireBlock(node: TSESTree.Node) { let n = node // Handle cases like `const baz = require('foo').bar.baz` // and `const foo = require('foo')()` while ( - (n.parent.type === 'MemberExpression' && n.parent.object === n) || - (n.parent.type === 'CallExpression' && n.parent.callee === n) + n.parent && + ((n.parent.type === 'MemberExpression' && n.parent.object === n) || + (n.parent.type === 'CallExpression' && n.parent.callee === n)) ) { n = n.parent } if ( - n.parent.type === 'VariableDeclarator' && - n.parent.parent.type === 'VariableDeclaration' && - n.parent.parent.parent.type === 'Program' + n.parent?.type === 'VariableDeclarator' && + n.parent.parent?.type === 'VariableDeclaration' && + n.parent.parent.parent?.type === 'Program' ) { return n.parent.parent.parent } @@ -500,28 +614,31 @@ const types = [ 'index', 'object', 'type', -] +] as const // Creates an object with type-rank pairs. // Example: { index: 0, sibling: 1, parent: 1, external: 1, builtin: 2, internal: 2 } // Will throw an error if it contains a type that does not exist, or has a duplicate -function convertGroupsToRanks(groups) { - const rankObject = groups.reduce(function (res, group, index) { - ;[].concat(group).forEach(function (groupItem) { - if (types.indexOf(groupItem) === -1) { - throw new Error( - `Incorrect configuration of the rule: Unknown type \`${JSON.stringify(groupItem)}\``, - ) - } - if (res[groupItem] !== undefined) { - throw new Error( - `Incorrect configuration of the rule: \`${groupItem}\` is duplicated`, - ) - } - res[groupItem] = index * 2 - }) - return res - }, {}) +function convertGroupsToRanks(groups: ReadonlyArray>) { + const rankObject = groups.reduce( + (res, group, index) => { + ;([group] as const).flat().forEach(groupItem => { + if (!types.includes(groupItem)) { + throw new Error( + `Incorrect configuration of the rule: Unknown type \`${JSON.stringify(groupItem)}\``, + ) + } + if (res[groupItem] !== undefined) { + throw new Error( + `Incorrect configuration of the rule: \`${groupItem}\` is duplicated`, + ) + } + res[groupItem] = index * 2 + }) + return res + }, + {} as Record, + ) const omittedTypes = types.filter(function (type) { return typeof rankObject[type] === 'undefined' @@ -535,9 +652,9 @@ function convertGroupsToRanks(groups) { return { groups: ranks, omittedTypes } } -function convertPathGroupsForRanks(pathGroups) { - const after = {} - const before = {} +function convertPathGroupsForRanks(pathGroups: PathGroup[]) { + const after: Record = {} + const before: Record = {} const transformed = pathGroups.map((pathGroup, index) => { const { group, position: positionString } = pathGroup @@ -579,7 +696,10 @@ function convertPathGroupsForRanks(pathGroups) { } } -function fixNewLineAfterImport(context, previousImport) { +function fixNewLineAfterImport( + context: RuleContext, + previousImport: ImportEntry, +) { const prevRoot = findRootNode(previousImport.node) const tokensToEndOfLine = takeTokensAfterWhile( context.getSourceCode(), @@ -591,33 +711,39 @@ function fixNewLineAfterImport(context, previousImport) { if (tokensToEndOfLine.length > 0) { endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1] } - return fixer => + return (fixer: TSESLint.RuleFixer) => fixer.insertTextAfterRange([prevRoot.range[0], endOfLine], '\n') } -function removeNewLineAfterImport(context, currentImport, previousImport) { +function removeNewLineAfterImport( + context: RuleContext, + currentImport: ImportEntry, + previousImport: ImportEntry, +) { const sourceCode = context.getSourceCode() const prevRoot = findRootNode(previousImport.node) const currRoot = findRootNode(currentImport.node) const rangeToRemove = [ findEndOfLineWithComments(sourceCode, prevRoot), findStartOfLineWithComments(sourceCode, currRoot), - ] + ] as const if ( /^\s*$/.test(sourceCode.text.substring(rangeToRemove[0], rangeToRemove[1])) ) { - return fixer => fixer.removeRange(rangeToRemove) + return (fixer: TSESLint.RuleFixer) => fixer.removeRange(rangeToRemove) } - return undefined } function makeNewlinesBetweenReport( - context, - imported, - newlinesBetweenImports, - distinctGroup, + context: RuleContext, + imported: ImportEntryWithRank[], + newlinesBetweenImports: Options['newlines-between'], + distinctGroup?: boolean, ) { - const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => { + const getNumberOfEmptyLinesBetween = ( + currentImport: ImportEntryWithRank, + previousImport: ImportEntryWithRank, + ) => { const linesBetweenImports = context .getSourceCode() .lines.slice( @@ -627,11 +753,13 @@ function makeNewlinesBetweenReport( return linesBetweenImports.filter(line => !line.trim().length).length } - const getIsStartOfDistinctGroup = (currentImport, previousImport) => - currentImport.rank - 1 >= previousImport.rank + const getIsStartOfDistinctGroup = ( + currentImport: ImportEntryWithRank, + previousImport: ImportEntryWithRank, + ) => currentImport.rank - 1 >= previousImport.rank let previousImport = imported[0] - imported.slice(1).forEach(function (currentImport) { + imported.slice(1).forEach(currentImport => { const emptyLinesBetween = getNumberOfEmptyLinesBetween( currentImport, previousImport, @@ -652,8 +780,7 @@ function makeNewlinesBetweenReport( if (distinctGroup || (!distinctGroup && isStartOfDistinctGroup)) { context.report({ node: previousImport.node, - message: - 'There should be at least one empty line between import groups', + messageId: 'oneLineBetweenGroups', fix: fixNewLineAfterImport(context, previousImport), }) } @@ -667,7 +794,7 @@ function makeNewlinesBetweenReport( ) { context.report({ node: previousImport.node, - message: 'There should be no empty line within import group', + messageId: 'noLineWithinGroup', fix: removeNewLineAfterImport( context, currentImport, @@ -679,7 +806,7 @@ function makeNewlinesBetweenReport( } else if (emptyLinesBetween > 0) { context.report({ node: previousImport.node, - message: 'There should be no empty line between import groups', + messageId: 'noLineBetweenGroups', fix: removeNewLineAfterImport(context, currentImport, previousImport), }) } @@ -688,27 +815,46 @@ function makeNewlinesBetweenReport( }) } -function getAlphabetizeConfig(options) { +function getAlphabetizeConfig(options: Options): AlphabetizeOptions { const alphabetize = options.alphabetize || {} const order = alphabetize.order || 'ignore' const orderImportKind = alphabetize.orderImportKind || 'ignore' const caseInsensitive = alphabetize.caseInsensitive || false - return { order, orderImportKind, caseInsensitive } } // TODO, semver-major: Change the default of "distinctGroup" from true to false const defaultDistinctGroup = true -module.exports = { +type Options = { + 'newlines-between'?: + | 'always' + | 'always-and-inside-groups' + | 'ignore' + | 'never' + alphabetize?: Partial + distinctGroup?: boolean + groups?: ReadonlyArray> + pathGroupsExcludedImportTypes?: ImportType[] + pathGroups?: PathGroup[] + warnOnUnassignedImports?: boolean +} + +type MessageId = + | 'error' + | 'noLineWithinGroup' + | 'noLineBetweenGroups' + | 'oneLineBetweenGroups' + | 'order' + +export = createRule<[Options?], MessageId>({ + name: 'order', meta: { type: 'suggestion', docs: { category: 'Style guide', description: 'Enforce a convention in module import order.', - url: docsUrl('order'), }, - fixable: 'code', schema: [ { @@ -777,12 +923,21 @@ module.exports = { additionalProperties: false, }, ], + messages: { + error: '{{error}}', + noLineWithinGroup: 'There should be no empty line within import group', + noLineBetweenGroups: + 'There should be no empty line between import groups', + oneLineBetweenGroups: + 'There should be at least one empty line between import groups', + order: '{{secondImport}} should occur {{order}} {{firstImport}}', + }, }, - - create: function importOrderRule(context) { + defaultOptions: [], + create(context) { const options = context.options[0] || {} const newlinesBetweenImports = options['newlines-between'] || 'ignore' - const pathGroupsExcludedImportTypes = new Set( + const pathGroupsExcludedImportTypes = new Set( options.pathGroupsExcludedImportTypes || [ 'builtin', 'external', @@ -794,7 +949,8 @@ module.exports = { options.distinctGroup == null ? defaultDistinctGroup : !!options.distinctGroup - let ranks + + let ranks: Ranks try { const { pathGroups, maxPosition } = convertPathGroupsForRanks( @@ -813,21 +969,28 @@ module.exports = { // Malformed configuration return { Program(node) { - context.report(node, error.message) + context.report({ + node, + messageId: 'error', + data: { + error: (error as Error).message, + }, + }) }, } } - const importMap = new Map() - function getBlockImports(node) { + const importMap = new Map() + + function getBlockImports(node: TSESTree.Node) { if (!importMap.has(node)) { importMap.set(node, []) } - return importMap.get(node) + return importMap.get(node)! } return { - ImportDeclaration: function handleImports(node) { + ImportDeclaration(node) { // Ignoring unassigned imports unless warnOnUnassignedImports is set if (node.specifiers.length || options.warnOnUnassignedImports) { const name = node.source.value @@ -840,20 +1003,24 @@ module.exports = { type: 'import', }, ranks, - getBlockImports(node.parent), + getBlockImports(node.parent!), pathGroupsExcludedImportTypes, ) } }, - TSImportEqualsDeclaration: function handleImports(node) { - let displayName - let value - let type + TSImportEqualsDeclaration(node) { + let displayName: string + let value: string + let type: 'import:object' | 'import' // skip "export import"s if (node.isExport) { return } - if (node.moduleReference.type === 'TSExternalModuleReference') { + if ( + node.moduleReference.type === 'TSExternalModuleReference' && + 'value' in node.moduleReference.expression && + typeof node.moduleReference.expression.value === 'string' + ) { value = node.moduleReference.expression.value displayName = value type = 'import' @@ -871,19 +1038,24 @@ module.exports = { type, }, ranks, - getBlockImports(node.parent), + getBlockImports(node.parent!), pathGroupsExcludedImportTypes, ) }, - CallExpression: function handleRequires(node) { + CallExpression(node) { if (!isStaticRequire(node)) { return } const block = getRequireBlock(node) - if (!block) { + const firstArg = node.arguments[0] + if ( + !block || + !('value' in firstArg) || + typeof firstArg.value !== 'string' + ) { return } - const name = node.arguments[0].value + const name = firstArg.value registerNode( context, { @@ -919,4 +1091,4 @@ module.exports = { }, } }, -} +}) diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.ts similarity index 63% rename from src/rules/prefer-default-export.js rename to src/rules/prefer-default-export.ts index ee849b0ed..f272f1dd6 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.ts @@ -1,20 +1,22 @@ 'use strict' -import { docsUrl } from '../docs-url' +import { TSESTree } from '@typescript-eslint/utils' +import { createRule } from '../utils' -const SINGLE_EXPORT_ERROR_MESSAGE = - 'Prefer default export on a file with single export.' -const ANY_EXPORT_ERROR_MESSAGE = - 'Prefer default export to be present on every file that has export.' +type Options = { + target?: 'single' | 'any' +} + +type MessageId = 'single' | 'any' -module.exports = { +export = createRule<[Options?], MessageId>({ + name: 'prefer-default-export', meta: { type: 'suggestion', docs: { category: 'Style guide', description: 'Prefer a default export if module exports a single name or multiple names.', - url: docsUrl('prefer-default-export'), }, schema: [ { @@ -29,26 +31,30 @@ module.exports = { additionalProperties: false, }, ], + messages: { + single: 'Prefer default export on a file with single export.', + any: 'Prefer default export to be present on every file that has export.', + }, }, - + defaultOptions: [], create(context) { let specifierExportCount = 0 let hasDefaultExport = false let hasStarExport = false let hasTypeExport = false - let namedExportNode = null + + let namedExportNode: TSESTree.Node + // get options. by default we look into files with single export const { target = 'single' } = context.options[0] || {} - function captureDeclaration(identifierOrPattern) { - if (identifierOrPattern && identifierOrPattern.type === 'ObjectPattern') { + + function captureDeclaration(identifierOrPattern?: TSESTree.Node | null) { + if (identifierOrPattern?.type === 'ObjectPattern') { // recursively capture - identifierOrPattern.properties.forEach(function (property) { + identifierOrPattern.properties.forEach(property => { captureDeclaration(property.value) }) - } else if ( - identifierOrPattern && - identifierOrPattern.type === 'ArrayPattern' - ) { + } else if (identifierOrPattern?.type === 'ArrayPattern') { identifierOrPattern.elements.forEach(captureDeclaration) } else { // assume it's a single standard identifier @@ -62,7 +68,10 @@ module.exports = { }, ExportSpecifier(node) { - if ((node.exported.name || node.exported.value) === 'default') { + if ( + (node.exported.name || + ('value' in node.exported && node.exported.value)) === 'default' + ) { hasDefaultExport = true } else { specifierExportCount++ @@ -80,8 +89,10 @@ module.exports = { if ( type === 'TSTypeAliasDeclaration' || - type === 'TypeAlias' || type === 'TSInterfaceDeclaration' || + // @ts-expect-error - legacy parser type + type === 'TypeAlias' || + // @ts-expect-error - legacy parser type type === 'InterfaceDeclaration' ) { specifierExportCount++ @@ -89,8 +100,11 @@ module.exports = { return } - if (node.declaration.declarations) { - node.declaration.declarations.forEach(function (declaration) { + if ( + 'declarations' in node.declaration && + node.declaration.declarations + ) { + node.declaration.declarations.forEach(declaration => { captureDeclaration(declaration.id) }) } else { @@ -114,11 +128,17 @@ module.exports = { return } if (target === 'single' && specifierExportCount === 1) { - context.report(namedExportNode, SINGLE_EXPORT_ERROR_MESSAGE) + context.report({ + node: namedExportNode, + messageId: 'single', + }) } else if (target === 'any' && specifierExportCount > 0) { - context.report(namedExportNode, ANY_EXPORT_ERROR_MESSAGE) + context.report({ + node: namedExportNode, + messageId: 'any', + }) } }, } }, -} +}) diff --git a/src/rules/unambiguous.js b/src/rules/unambiguous.ts similarity index 66% rename from src/rules/unambiguous.js rename to src/rules/unambiguous.ts index 9f4a673b0..e6e7b2454 100644 --- a/src/rules/unambiguous.js +++ b/src/rules/unambiguous.ts @@ -1,23 +1,25 @@ /** - * @fileOverview Report modules that could parse incorrectly as scripts. - * @author Ben Mosher + * Report modules that could parse incorrectly as scripts. */ import { isUnambiguousModule } from '../utils/unambiguous' -import { docsUrl } from '../docs-url' +import { createRule } from '../utils' -module.exports = { +export = createRule({ + name: 'unambiguous', meta: { type: 'suggestion', docs: { category: 'Module systems', description: 'Forbid potentially ambiguous parse goal (`script` vs. `module`).', - url: docsUrl('unambiguous'), }, schema: [], + messages: { + module: 'This module could be parsed as a valid script.', + }, }, - + defaultOptions: [], create(context) { // ignore non-modules if (context.parserOptions.sourceType !== 'module') { @@ -29,10 +31,10 @@ module.exports = { if (!isUnambiguousModule(ast)) { context.report({ node: ast, - message: 'This module could be parsed as a valid script.', + messageId: 'module', }) } }, } }, -} +}) diff --git a/src/types.ts b/src/types.ts index fc4bd9e8a..8e02132bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,13 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils' import type { TsResolverOptions } from 'eslint-import-resolver-typescript' -import type { KebabCase, LiteralUnion } from 'type-fest' import type { ResolveOptions } from 'enhanced-resolve' +import type { MinimatchOptions } from 'minimatch' +import type { KebabCase, LiteralUnion } from 'type-fest' +import type { ImportType as ImportType_ } from './core/import-type' import type { PluginName } from './utils' -import { TSESLint, TSESTree } from '@typescript-eslint/utils' + +export type ImportType = ImportType_ | 'object' | 'type' export interface NodeResolverOptions { extensions?: readonly string[] @@ -24,6 +28,15 @@ export type DocStyle = 'jsdoc' | 'tomdoc' export type Arrayable = T | readonly T[] +export type ImportResolver = + | LiteralUnion<'node' | 'typescript' | 'webpack', string> + | { + node?: boolean | NodeResolverOptions + typescript?: boolean | TsResolverOptions + webpack?: WebpackResolverOptions + [resolve: string]: unknown + } + export interface ImportSettings { cache?: { lifetime?: number | '∞' | 'Infinity' @@ -36,15 +49,7 @@ export interface ImportSettings { internalRegex?: string parsers?: Record resolve?: NodeResolverOptions - resolver?: Arrayable< - | LiteralUnion<'node' | 'typescript' | 'webpack', string> - | { - node?: boolean | NodeResolverOptions - typescript?: boolean | TsResolverOptions - webpack?: WebpackResolverOptions - [resolve: string]: unknown - } - > + resolver?: Arrayable } export type WithPluginName = T extends string @@ -100,3 +105,16 @@ export type ExportNamespaceSpecifier = CustomESTreeNode< 'ExportNamespaceSpecifier', { exported: TSESTree.Identifier } > + +export interface PathGroup { + pattern: string + group: ImportType + patternOptions?: MinimatchOptions + position?: 'before' | 'after' +} + +export interface AlphabetizeOptions { + caseInsensitive: boolean + order: 'ignore' | 'asc' | 'desc' + orderImportKind: 'ignore' | 'asc' | 'desc' +} diff --git a/src/utils/resolve.ts b/src/utils/resolve.ts index b74bae02c..94ce81030 100644 --- a/src/utils/resolve.ts +++ b/src/utils/resolve.ts @@ -6,6 +6,7 @@ import path from 'path' import type { Arrayable, + ImportResolver, ImportSettings, PluginSettings, RuleContext, @@ -188,9 +189,7 @@ function fullResolve( const resolvers = resolverReducer(configResolvers, new Map()) - for (const pair of resolvers) { - const name = pair[0] - const config = pair[1] + for (const [name, config] of resolvers) { const resolver = requireResolver(name, sourceFile) const resolved = withResolver(resolver, config) @@ -217,11 +216,11 @@ export function relative( } function resolverReducer( - resolvers: Arrayable>, + resolvers: Arrayable, map: Map, ) { if (Array.isArray(resolvers)) { - resolvers.forEach(r => resolverReducer(r, map)) + ;(resolvers as ImportResolver[]).forEach(r => resolverReducer(r, map)) return map } diff --git a/test/rules/dynamic-import-chunkname.spec.js b/test/rules/dynamic-import-chunkname.spec.ts similarity index 88% rename from test/rules/dynamic-import-chunkname.spec.js rename to test/rules/dynamic-import-chunkname.spec.ts index 2b3943093..1529c8be6 100644 --- a/test/rules/dynamic-import-chunkname.spec.js +++ b/test/rules/dynamic-import-chunkname.spec.ts @@ -1,42 +1,47 @@ +import { TSESLint, TSESTree } from '@typescript-eslint/utils' + +import rule from '../../src/rules/dynamic-import-chunkname' + import { SYNTAX_CASES, parsers } from '../utils' -import { RuleTester } from 'eslint' -const rule = require('rules/dynamic-import-chunkname') -const ruleTester = new RuleTester() +const ruleTester = new TSESLint.RuleTester() -const commentFormat = '([0-9a-zA-Z-_/.]|\\[(request|index)\\])+' const pickyCommentFormat = '[a-zA-Z-_/.]+' -const options = [{ importFunctions: ['dynamicImport'] }] + +const options = [ + { + importFunctions: ['dynamicImport'], + }, +] as const + const pickyCommentOptions = [ { importFunctions: ['dynamicImport'], webpackChunknameFormat: pickyCommentFormat, }, -] +] as const + const allowEmptyOptions = [ { importFunctions: ['dynamicImport'], allowEmpty: true, }, -] +] as const + const multipleImportFunctionOptions = [ { importFunctions: ['dynamicImport', 'definitelyNotStaticImport'], }, -] +] as const + const parser = parsers.BABEL -const noLeadingCommentError = - 'dynamic imports require a leading comment with the webpack chunkname' -const nonBlockCommentError = - 'dynamic imports require a /* foo */ style comment, not a // foo comment' -const noPaddingCommentError = - 'dynamic imports require a block comment padded with spaces - /* foo */' -const invalidSyntaxCommentError = - 'dynamic imports require a "webpack" comment with valid syntax' -const commentFormatError = `dynamic imports require a "webpack" comment with valid syntax` -const chunkNameFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: ["']${commentFormat}["'],? */` -const pickyChunkNameFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: ["']${pickyCommentFormat}["'],? */` +const pickyChunkNameFormatError = { + messageId: 'chunknameFormat', + data: { + format: ` webpackChunkName: ["']${pickyCommentFormat}["'],? `, + }, +} as const ruleTester.run('dynamic-import-chunkname', rule, { valid: [ @@ -445,8 +450,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: nonBlockCommentError, - type: 'ImportExpression', + messageId: 'blockComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -457,8 +462,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { output: "import('test')", errors: [ { - message: noLeadingCommentError, - type: 'ImportExpression', + messageId: 'leadingComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -475,8 +480,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -493,8 +498,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -511,8 +516,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -529,8 +534,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -547,8 +552,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -565,8 +570,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: chunkNameFormatError, - type: 'ImportExpression', + messageId: 'chunknameFormat', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -583,8 +588,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: chunkNameFormatError, - type: 'ImportExpression', + messageId: 'chunknameFormat', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -601,8 +606,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: chunkNameFormatError, - type: 'ImportExpression', + messageId: 'chunknameFormat', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -619,8 +624,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: noPaddingCommentError, - type: 'ImportExpression', + messageId: 'paddedSpaces', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -637,8 +642,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -655,8 +660,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -673,8 +678,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -693,8 +698,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -711,8 +716,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -729,8 +734,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: pickyChunkNameFormatError, - type: 'ImportExpression', + ...pickyChunkNameFormatError, + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -747,8 +752,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -765,8 +770,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -783,8 +788,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -801,8 +806,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -819,8 +824,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -837,8 +842,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -855,8 +860,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -873,8 +878,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -891,8 +896,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -909,8 +914,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -927,8 +932,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'ImportExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.ImportExpression, }, ], }, @@ -944,8 +949,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'CallExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -961,8 +966,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'CallExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -978,8 +983,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: nonBlockCommentError, - type: 'CallExpression', + messageId: 'blockComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -989,8 +994,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { output: "dynamicImport('test')", errors: [ { - message: noLeadingCommentError, - type: 'CallExpression', + messageId: 'leadingComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -1006,8 +1011,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'CallExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -1023,8 +1028,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: invalidSyntaxCommentError, - type: 'CallExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -1040,8 +1045,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: commentFormatError, - type: 'CallExpression', + messageId: 'webpackComment', + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -1057,8 +1062,8 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, errors: [ { - message: pickyChunkNameFormatError, - type: 'CallExpression', + ...pickyChunkNameFormatError, + type: TSESTree.AST_NODE_TYPES.CallExpression, }, ], }, @@ -1067,7 +1072,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { describe('TypeScript', () => { const typescriptParser = parsers.TS - const nodeType = 'ImportExpression' + const nodeType = TSESTree.AST_NODE_TYPES.ImportExpression ruleTester.run('dynamic-import-chunkname', rule, { valid: [ @@ -1400,7 +1405,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: nonBlockCommentError, + messageId: 'blockComment', type: nodeType, }, ], @@ -1412,7 +1417,7 @@ describe('TypeScript', () => { output: "import('test')", errors: [ { - message: noLeadingCommentError, + messageId: 'leadingComment', type: nodeType, }, ], @@ -1430,7 +1435,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: invalidSyntaxCommentError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1448,7 +1453,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: invalidSyntaxCommentError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1466,7 +1471,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: invalidSyntaxCommentError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1484,7 +1489,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: invalidSyntaxCommentError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1502,7 +1507,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1520,7 +1525,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: noPaddingCommentError, + messageId: 'paddedSpaces', type: nodeType, }, ], @@ -1538,7 +1543,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1556,7 +1561,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: invalidSyntaxCommentError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1574,7 +1579,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: invalidSyntaxCommentError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1594,7 +1599,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1612,7 +1617,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1630,7 +1635,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: chunkNameFormatError, + messageId: 'chunknameFormat', type: nodeType, }, ], @@ -1648,7 +1653,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: chunkNameFormatError, + messageId: 'chunknameFormat', type: nodeType, }, ], @@ -1666,7 +1671,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: chunkNameFormatError, + messageId: 'chunknameFormat', type: nodeType, }, ], @@ -1684,7 +1689,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: pickyChunkNameFormatError, + ...pickyChunkNameFormatError, type: nodeType, }, ], @@ -1702,7 +1707,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1720,7 +1725,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1738,7 +1743,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1756,7 +1761,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1774,7 +1779,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1792,7 +1797,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1810,7 +1815,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1828,7 +1833,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1846,7 +1851,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1864,7 +1869,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], @@ -1882,7 +1887,7 @@ describe('TypeScript', () => { )`, errors: [ { - message: commentFormatError, + messageId: 'webpackComment', type: nodeType, }, ], diff --git a/test/rules/exports-last.spec.js b/test/rules/exports-last.spec.ts similarity index 89% rename from test/rules/exports-last.spec.js rename to test/rules/exports-last.spec.ts index cc853ba76..13b4ddffa 100644 --- a/test/rules/exports-last.spec.js +++ b/test/rules/exports-last.spec.ts @@ -1,14 +1,16 @@ -import { test } from '../utils' +import { TSESTree, TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/exports-last' -import { RuleTester } from 'eslint' -import rule from 'rules/exports-last' +import { test } from '../utils' -const ruleTester = new RuleTester() +const ruleTester = new TSESLint.RuleTester() -const error = type => ({ - message: 'Export statements should appear at the end of the file', - type, -}) +const error = (type: `${TSESTree.AST_NODE_TYPES}`) => + ({ + messageId: 'end', + type, + }) as const ruleTester.run('exports-last', rule, { valid: [ diff --git a/test/rules/first.spec.js b/test/rules/first.spec.ts similarity index 89% rename from test/rules/first.spec.js rename to test/rules/first.spec.ts index 63f03684e..20a54f5b1 100644 --- a/test/rules/first.spec.js +++ b/test/rules/first.spec.ts @@ -1,14 +1,15 @@ -import { test, parsers, testVersion } from '../utils' import fs from 'fs' -import path from 'path' -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' -const ruleTester = new RuleTester() -const rule = require('rules/first') +import rule from '../../src/rules/first' + +import { test, parsers, testFilePath } from '../utils' + +const ruleTester = new TSESLint.RuleTester() ruleTester.run('first', rule, { - valid: [].concat( + valid: [ test({ code: "import { x } from './foo'; import { y } from './bar';\ export { x, y }", @@ -23,14 +24,12 @@ ruleTester.run('first', rule, { code: "'use directive';\ import { x } from 'foo';", }), - testVersion('>= 7', () => ({ + test({ // issue #2210 - code: String( - fs.readFileSync(path.join(__dirname, '../fixtures/component.html')), - ), + code: fs.readFileSync(testFilePath('component.html'), 'utf8'), parser: require.resolve('@angular-eslint/template-parser'), - })), - ), + }), + ], invalid: [ test({ code: "import { x } from './foo';\ diff --git a/test/rules/group-exports.spec.ts b/test/rules/group-exports.spec.ts index 0df763d54..13f44d7c7 100644 --- a/test/rules/group-exports.spec.ts +++ b/test/rules/group-exports.spec.ts @@ -18,7 +18,7 @@ const ruleTester = new TSESLint.RuleTester({ babelOptions: { configFile: false, babelrc: false, - presets: ['@babel/preset-flow'], + presets: ['@babel/flow'], }, }, }) diff --git a/test/rules/max-dependencies.spec.js b/test/rules/max-dependencies.spec.ts similarity index 65% rename from test/rules/max-dependencies.spec.js rename to test/rules/max-dependencies.spec.ts index aad625aae..764a23593 100644 --- a/test/rules/max-dependencies.spec.js +++ b/test/rules/max-dependencies.spec.ts @@ -1,9 +1,10 @@ -import { test, parsers } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/max-dependencies' -import { RuleTester } from 'eslint' +import { test, parsers } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/max-dependencies') +const ruleTester = new TSESLint.RuleTester() ruleTester.run('max-dependencies', rule, { valid: [ @@ -98,46 +99,42 @@ ruleTester.run('max-dependencies', rule, { describe('TypeScript', () => { const parser = parsers.TS - ruleTester.run( - `max-dependencies (${parser.replace(process.cwd(), '.')})`, - rule, - { - valid: [ - test({ - code: "import type { x } from './foo'; import { y } from './bar';", - parser, - options: [ - { - max: 1, - ignoreTypeImports: true, - }, - ], - }), - ], - invalid: [ - test({ - code: "import type { x } from './foo'; import type { y } from './bar'", - parser, - options: [ - { - max: 1, - }, - ], - errors: ['Maximum number of dependencies (1) exceeded.'], - }), + ruleTester.run('max-dependencies', rule, { + valid: [ + test({ + code: "import type { x } from './foo'; import { y } from './bar';", + parser, + options: [ + { + max: 1, + ignoreTypeImports: true, + }, + ], + }), + ], + invalid: [ + test({ + code: "import type { x } from './foo'; import type { y } from './bar'", + parser, + options: [ + { + max: 1, + }, + ], + errors: ['Maximum number of dependencies (1) exceeded.'], + }), - test({ - code: "import type { x } from './foo'; import type { y } from './bar'; import type { z } from './baz'", - parser, - options: [ - { - max: 2, - ignoreTypeImports: false, - }, - ], - errors: ['Maximum number of dependencies (2) exceeded.'], - }), - ], - }, - ) + test({ + code: "import type { x } from './foo'; import type { y } from './bar'; import type { z } from './baz'", + parser, + options: [ + { + max: 2, + ignoreTypeImports: false, + }, + ], + errors: ['Maximum number of dependencies (2) exceeded.'], + }), + ], + }) }) diff --git a/test/rules/newline-after-import.spec.js b/test/rules/newline-after-import.spec.ts similarity index 90% rename from test/rules/newline-after-import.spec.js rename to test/rules/newline-after-import.spec.ts index 17e90586a..1b323b41e 100644 --- a/test/rules/newline-after-import.spec.js +++ b/test/rules/newline-after-import.spec.ts @@ -1,18 +1,35 @@ -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' -import { parsers, testVersion } from '../utils' +import rule from '../../src/rules/newline-after-import' -const IMPORT_ERROR_MESSAGE = - 'Expected 1 empty line after import statement not followed by another import.' -const IMPORT_ERROR_MESSAGE_MULTIPLE = count => - `Expected ${count} empty lines after import statement not followed by another import.` -const REQUIRE_ERROR_MESSAGE = - 'Expected 1 empty line after require statement not followed by another require.' +import { parsers } from '../utils' -const ruleTester = new RuleTester() +const ruleTester = new TSESLint.RuleTester() -ruleTester.run('newline-after-import', require('rules/newline-after-import'), { - valid: [].concat( +const getImportError = (count: number) => ({ + messageId: 'newline' as const, + data: { + count, + lineSuffix: count > 1 ? 's' : '', + type: 'import', + }, +}) + +const IMPORT_ERROR = getImportError(1) + +const getRequireError = (count: number) => ({ + messageId: 'newline' as const, + data: { + count, + lineSuffix: count > 1 ? 's' : '', + type: 'require', + }, +}) + +const REQUIRE_ERROR = getRequireError(1) + +ruleTester.run('newline-after-import', rule, { + valid: [ `var path = require('path');\nvar foo = require('foo');\n`, `require('foo');`, `switch ('foo') { case 'bar': require('baz'); }`, @@ -395,9 +412,9 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { `, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, - ), + ], - invalid: [].concat( + invalid: [ { code: ` import { A, B, C, D } from @@ -416,7 +433,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 3, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -445,7 +462,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 3, column: 9, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -468,7 +485,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 3, column: 9, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -481,7 +498,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -494,7 +511,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -506,7 +523,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -519,7 +536,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -531,12 +548,12 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, { line: 4, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -548,12 +565,12 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, { line: 4, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -565,12 +582,12 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, { line: 4, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -582,7 +599,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 2, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], }, @@ -593,7 +610,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 2, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], }, @@ -604,7 +621,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 3, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], }, @@ -615,7 +632,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 6, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], }, @@ -626,7 +643,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 2, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -638,7 +655,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 25, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -650,7 +667,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -663,7 +680,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -676,7 +693,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 2, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { sourceType: 'module' }, @@ -689,13 +706,13 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 2, column: 1, - message: REQUIRE_ERROR_MESSAGE, + ...REQUIRE_ERROR, }, ], parserOptions: { sourceType: 'module' }, parser: parsers.BABEL, }, - testVersion('>= 6', () => ({ + { code: ` // issue 1784 import { map } from 'rxjs/operators'; @@ -713,12 +730,12 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 3, column: 9, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { sourceType: 'module' }, parser: parsers.BABEL, - })) || [], + }, { code: `import foo from 'foo';\n\nexport default function() {};`, output: `import foo from 'foo';\n\n\nexport default function() {};`, @@ -727,7 +744,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -740,7 +757,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -753,7 +770,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -766,7 +783,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -779,7 +796,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -792,7 +809,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -805,7 +822,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -818,7 +835,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -831,7 +848,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + ...getImportError(2), }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -855,7 +872,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 2, column: 9, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -869,7 +886,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, @@ -882,8 +899,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: - 'Expected 2 empty lines after require statement not followed by another require.', + ...getRequireError(2), }, ], parserOptions: { ecmaVersion: 2015 }, @@ -896,8 +912,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: - 'Expected 2 empty lines after require statement not followed by another require.', + ...getRequireError(2), }, ], parserOptions: { ecmaVersion: 2015 }, @@ -910,7 +925,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + ...IMPORT_ERROR, }, ], parserOptions: { @@ -921,16 +936,16 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }, { code: `const foo = require('foo');\n\n\n// some random comment\nconst bar = function() {};`, + output: null, options: [{ count: 2, exactCount: true, considerComments: true }], errors: [ { line: 1, column: 1, - message: - 'Expected 2 empty lines after require statement not followed by another require.', + ...getRequireError(2), }, ], parserOptions: { ecmaVersion: 2015 }, }, - ), + ], }) diff --git a/test/rules/no-absolute-path.spec.js b/test/rules/no-absolute-path.spec.ts similarity index 95% rename from test/rules/no-absolute-path.spec.js rename to test/rules/no-absolute-path.spec.ts index b99b986b7..19f1309d9 100644 --- a/test/rules/no-absolute-path.spec.js +++ b/test/rules/no-absolute-path.spec.ts @@ -1,12 +1,13 @@ -import { test } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-absolute-path' -import { RuleTester } from 'eslint' +import { test } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/no-absolute-path') +const ruleTester = new TSESLint.RuleTester() const error = { - message: 'Do not import modules using an absolute path', + messageId: 'absolute', } ruleTester.run('no-absolute-path', rule, { diff --git a/test/rules/no-amd.spec.js b/test/rules/no-amd.spec.js deleted file mode 100644 index 5a2add993..000000000 --- a/test/rules/no-amd.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import { RuleTester } from 'eslint' -import eslintPkg from 'eslint/package.json' -import semver from 'semver' - -const ruleTester = new RuleTester({ - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, -}) - -ruleTester.run('no-amd', require('rules/no-amd'), { - valid: [ - { - code: 'import "x";', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: 'import x from "x"', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - 'var x = require("x")', - - 'require("x")', - // 2-args, not an array - 'require("x", "y")', - // random other function - 'setTimeout(foo, 100)', - // non-identifier callee - '(a || b)(1, 2, 3)', - - // nested scope is fine - 'function x() { define(["a"], function (a) {}) }', - 'function x() { require(["a"], function (a) {}) }', - - // unmatched arg types/number - 'define(0, 1, 2)', - 'define("a")', - ], - - invalid: semver.satisfies(eslintPkg.version, '< 4.0.0') - ? [] - : [ - { - code: 'define([], function() {})', - errors: [{ message: 'Expected imports instead of AMD define().' }], - }, - { - code: 'define(["a"], function(a) { console.log(a); })', - errors: [{ message: 'Expected imports instead of AMD define().' }], - }, - - { - code: 'require([], function() {})', - errors: [{ message: 'Expected imports instead of AMD require().' }], - }, - { - code: 'require(["a"], function(a) { console.log(a); })', - errors: [{ message: 'Expected imports instead of AMD require().' }], - }, - ], -}) diff --git a/test/rules/no-amd.spec.ts b/test/rules/no-amd.spec.ts new file mode 100644 index 000000000..6db511548 --- /dev/null +++ b/test/rules/no-amd.spec.ts @@ -0,0 +1,83 @@ +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-amd' + +const ruleTester = new TSESLint.RuleTester({ + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, +}) + +ruleTester.run('no-amd', rule, { + valid: [ + 'import "x";', + 'import x from "x"', + 'var x = require("x")', + + 'require("x")', + // 2-args, not an array + 'require("x", "y")', + // random other function + 'setTimeout(foo, 100)', + // non-identifier callee + '(a || b)(1, 2, 3)', + + // nested scope is fine + 'function x() { define(["a"], function (a) {}) }', + 'function x() { require(["a"], function (a) {}) }', + + // unmatched arg types/number + 'define(0, 1, 2)', + 'define("a")', + ], + + invalid: [ + { + code: 'define([], function() {})', + output: null, + errors: [ + { + messageId: 'amd', + data: { + type: 'define', + }, + }, + ], + }, + { + code: 'define(["a"], function(a) { console.log(a); })', + output: null, + errors: [ + { + messageId: 'amd', + data: { + type: 'define', + }, + }, + ], + }, + + { + code: 'require([], function() {})', + output: null, + errors: [ + { + messageId: 'amd', + data: { + type: 'require', + }, + }, + ], + }, + { + code: 'require(["a"], function(a) { console.log(a); })', + output: null, + errors: [ + { + messageId: 'amd', + data: { + type: 'require', + }, + }, + ], + }, + ], +}) diff --git a/test/rules/no-commonjs.spec.js b/test/rules/no-commonjs.spec.ts similarity index 50% rename from test/rules/no-commonjs.spec.js rename to test/rules/no-commonjs.spec.ts index 4d196c3c2..70139978a 100644 --- a/test/rules/no-commonjs.spec.js +++ b/test/rules/no-commonjs.spec.ts @@ -1,47 +1,29 @@ -import { RuleTester } from 'eslint' -import eslintPkg from 'eslint/package.json' -import semver from 'semver' +import { TSESLint } from '@typescript-eslint/utils' -const EXPORT_MESSAGE = 'Expected "export" or "export default"' -const IMPORT_MESSAGE = 'Expected "import" instead of "require()"' +import rule from '../../src/rules/no-commonjs' -const ruleTester = new RuleTester({ - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, +const EXPORT = 'export' +const IMPORT = 'import' + +const ruleTester = new TSESLint.RuleTester({ + parserOptions: { + ecmaVersion: 2015, + sourceType: 'module', + }, }) -ruleTester.run('no-commonjs', require('rules/no-commonjs'), { +ruleTester.run('no-commonjs', rule, { valid: [ // imports - { - code: 'import "x";', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: 'import x from "x"', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: 'import { x } from "x"', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, + { code: 'import "x";' }, + { code: 'import x from "x"' }, + { code: 'import { x } from "x"' }, // exports + { code: 'export default "x"' }, + { code: 'export function house() {}' }, { - code: 'export default "x"', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: 'export function house() {}', - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: ` - function someFunc() { - const exports = someComputation(); - expect(exports.someProp).toEqual({ a: 'value' }); - } - `, - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + code: ` function someFunc() { const exports = someComputation(); expect(exports.someProp).toEqual({ a: 'value' }); }`, }, // allowed requires @@ -53,10 +35,7 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { { code: "var bar = require('./bar', true);" }, { code: "var bar = proxyquire('./bar');" }, { code: "var bar = require('./ba' + 'r');" }, - { - code: 'var bar = require(`x${1}`);', - parserOptions: { ecmaVersion: 2015 }, - }, + { code: 'var bar = require(`x${1}`);' }, { code: 'var zero = require(0);' }, { code: 'require("x")', options: [{ allowRequire: true }] }, @@ -110,88 +89,82 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { invalid: [ // imports - ...(semver.satisfies(eslintPkg.version, '< 4.0.0') - ? [] - : [ - { - code: 'var x = require("x")', - output: 'var x = require("x")', - errors: [{ message: IMPORT_MESSAGE }], - }, - { - code: 'x = require("x")', - output: 'x = require("x")', - errors: [{ message: IMPORT_MESSAGE }], - }, - { - code: 'require("x")', - output: 'require("x")', - errors: [{ message: IMPORT_MESSAGE }], - }, - { - code: 'require(`x`)', - parserOptions: { ecmaVersion: 2015 }, - output: 'require(`x`)', - errors: [{ message: IMPORT_MESSAGE }], - }, - - { - code: 'if (typeof window !== "undefined") require("x")', - options: [{ allowConditionalRequire: false }], - output: 'if (typeof window !== "undefined") require("x")', - errors: [{ message: IMPORT_MESSAGE }], - }, - { - code: 'if (typeof window !== "undefined") { require("x") }', - options: [{ allowConditionalRequire: false }], - output: 'if (typeof window !== "undefined") { require("x") }', - errors: [{ message: IMPORT_MESSAGE }], - }, - { - code: 'try { require("x") } catch (error) {}', - options: [{ allowConditionalRequire: false }], - output: 'try { require("x") } catch (error) {}', - errors: [{ message: IMPORT_MESSAGE }], - }, - ]), + { + code: 'var x = require("x")', + output: 'var x = require("x")', + errors: [{ messageId: IMPORT }], + }, + { + code: 'x = require("x")', + output: 'x = require("x")', + errors: [{ messageId: IMPORT }], + }, + { + code: 'require("x")', + output: 'require("x")', + errors: [{ messageId: IMPORT }], + }, + { + code: 'require(`x`)', + output: 'require(`x`)', + errors: [{ messageId: IMPORT }], + }, + { + code: 'if (typeof window !== "undefined") require("x")', + options: [{ allowConditionalRequire: false }], + output: 'if (typeof window !== "undefined") require("x")', + errors: [{ messageId: IMPORT }], + }, + { + code: 'if (typeof window !== "undefined") { require("x") }', + options: [{ allowConditionalRequire: false }], + output: 'if (typeof window !== "undefined") { require("x") }', + errors: [{ messageId: IMPORT }], + }, + { + code: 'try { require("x") } catch (error) {}', + options: [{ allowConditionalRequire: false }], + output: 'try { require("x") } catch (error) {}', + errors: [{ messageId: IMPORT }], + }, // exports { code: 'exports.face = "palm"', output: 'exports.face = "palm"', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, { code: 'module.exports.face = "palm"', output: 'module.exports.face = "palm"', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, { code: 'module.exports = face', output: 'module.exports = face', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, { code: 'exports = module.exports = {}', output: 'exports = module.exports = {}', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, { code: 'var x = module.exports = {}', output: 'var x = module.exports = {}', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, { code: 'module.exports = {}', options: ['allow-primitive-modules'], output: 'module.exports = {}', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, { code: 'var x = module.exports', options: ['allow-primitive-modules'], output: 'var x = module.exports', - errors: [{ message: EXPORT_MESSAGE }], + errors: [{ messageId: EXPORT }], }, ], }) diff --git a/test/rules/no-default-export.spec.js b/test/rules/no-default-export.spec.ts similarity index 90% rename from test/rules/no-default-export.spec.js rename to test/rules/no-default-export.spec.ts index 754ceba9a..4cab63e27 100644 --- a/test/rules/no-default-export.spec.js +++ b/test/rules/no-default-export.spec.ts @@ -1,9 +1,10 @@ -import { parsers, test, testVersion } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-default-export' -import { RuleTester } from 'eslint' +import { parsers, test, testVersion } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/no-default-export') +const ruleTester = new TSESLint.RuleTester() ruleTester.run('no-default-export', rule, { valid: [ @@ -85,8 +86,8 @@ ruleTester.run('no-default-export', rule, { parser: parsers.BABEL, }), ], - invalid: [].concat( - testVersion('> 2', () => ({ + invalid: [ + test({ code: 'export default function bar() {};', errors: [ { @@ -96,8 +97,8 @@ ruleTester.run('no-default-export', rule, { column: 8, }, ], - })), - testVersion('> 2', () => ({ + }), + test({ code: ` export const foo = 'foo'; export default bar;`, @@ -109,8 +110,8 @@ ruleTester.run('no-default-export', rule, { column: 16, }, ], - })), - testVersion('> 2', () => ({ + }), + test({ code: 'export default class Bar {};', errors: [ { @@ -120,8 +121,8 @@ ruleTester.run('no-default-export', rule, { column: 8, }, ], - })), - testVersion('> 2', () => ({ + }), + test({ code: 'export default function() {};', errors: [ { @@ -131,8 +132,8 @@ ruleTester.run('no-default-export', rule, { column: 8, }, ], - })), - testVersion('> 2', () => ({ + }), + test({ code: 'export default class {};', errors: [ { @@ -142,7 +143,7 @@ ruleTester.run('no-default-export', rule, { column: 8, }, ], - })), + }), test({ code: 'let foo; export { foo as default }', errors: [ @@ -164,7 +165,7 @@ ruleTester.run('no-default-export', rule, { ], }), // es2022: Arbitrary module namespae identifier names - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'let foo; export { foo as "default" }', errors: [ { @@ -175,5 +176,5 @@ ruleTester.run('no-default-export', rule, { ], parserOptions: { ecmaVersion: 2022 }, })), - ), + ], }) diff --git a/test/rules/no-deprecated.spec.js b/test/rules/no-deprecated.spec.ts similarity index 98% rename from test/rules/no-deprecated.spec.js rename to test/rules/no-deprecated.spec.ts index fccebc7eb..3f2f270cb 100644 --- a/test/rules/no-deprecated.spec.js +++ b/test/rules/no-deprecated.spec.ts @@ -1,9 +1,10 @@ -import { test, SYNTAX_CASES, parsers } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-deprecated' -import { RuleTester } from 'eslint' +import { test, SYNTAX_CASES, parsers } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/no-deprecated') +const ruleTester = new TSESLint.RuleTester() ruleTester.run('no-deprecated', rule, { valid: [ diff --git a/test/rules/no-duplicates.spec.js b/test/rules/no-duplicates.spec.ts similarity index 98% rename from test/rules/no-duplicates.spec.js rename to test/rules/no-duplicates.spec.ts index 4725135bd..de7308c8f 100644 --- a/test/rules/no-duplicates.spec.js +++ b/test/rules/no-duplicates.spec.ts @@ -1,23 +1,18 @@ -import * as path from 'path' +import path from 'path' + +import { TSESLint } from '@typescript-eslint/utils' + import { - test as testUtil, + test, parsers, tsVersionSatisfies, typescriptEslintParserSatisfies, } from '../utils' import jsxConfig from '../../src/config/react' -import { RuleTester } from 'eslint' -import eslintPkg from 'eslint/package.json' -import semver from 'semver' - -const ruleTester = new RuleTester() -const rule = require('rules/no-duplicates') +import rule from '../../src/rules/no-duplicates' -// autofix only possible with eslint 4+ -const test = semver.satisfies(eslintPkg.version, '< 4') - ? t => testUtil({ ...t, output: t.code }) - : testUtil +const ruleTester = new TSESLint.RuleTester() ruleTester.run('no-duplicates', rule, { valid: [ diff --git a/test/rules/no-dynamic-require.spec.js b/test/rules/no-dynamic-require.spec.ts similarity index 86% rename from test/rules/no-dynamic-require.spec.js rename to test/rules/no-dynamic-require.spec.ts index b2d759cf5..37106edb6 100644 --- a/test/rules/no-dynamic-require.spec.js +++ b/test/rules/no-dynamic-require.spec.ts @@ -1,17 +1,18 @@ -import { parsers, test, testVersion } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' -import { RuleTester } from 'eslint' +import rule from '../../src/rules/no-dynamic-require' -const ruleTester = new RuleTester() -const rule = require('rules/no-dynamic-require') +import { ValidTestCase, parsers, test } from '../utils' + +const ruleTester = new TSESLint.RuleTester() const error = { - message: 'Calls to require() should use string literals', -} + messageId: 'require', +} as const const dynamicImportError = { - message: 'Calls to import() should use string literals', -} + messageId: 'import', +} as const ruleTester.run('no-dynamic-require', rule, { valid: [ @@ -29,11 +30,9 @@ ruleTester.run('no-dynamic-require', rule, { //dynamic import ...[parsers.ESPREE, parsers.BABEL].flatMap(parser => { - const _test = - parser === parsers.ESPREE - ? testObj => testVersion('>= 6.2.0', () => testObj) - : testObj => test(testObj) - return [].concat( + const _test = (testObj: T) => + parser === parsers.ESPREE ? testObj : test(testObj) + return [ _test({ code: 'import("foo")', options: [{ esmodule: true }], @@ -114,7 +113,7 @@ ruleTester.run('no-dynamic-require', rule, { ecmaVersion: 2020, }, }), - ) + ] }), ], invalid: [ @@ -142,11 +141,9 @@ ruleTester.run('no-dynamic-require', rule, { // dynamic import ...[parsers.ESPREE, parsers.BABEL].flatMap(parser => { - const _test = - parser === parsers.ESPREE - ? testObj => testVersion('>= 6.2.0', () => testObj) - : testObj => test(testObj) - return [].concat( + const _test = (testObj: T) => + parser === parsers.ESPREE ? testObj : test(testObj) + return [ _test({ code: 'import("../" + name)', errors: [dynamicImportError], @@ -183,7 +180,7 @@ ruleTester.run('no-dynamic-require', rule, { ecmaVersion: 2020, }, }), - ) + ] }), test({ code: 'require(`foo${x}`)', diff --git a/test/rules/no-empty-named-blocks.spec.js b/test/rules/no-empty-named-blocks.spec.ts similarity index 62% rename from test/rules/no-empty-named-blocks.spec.js rename to test/rules/no-empty-named-blocks.spec.ts index c47c59b15..a6aab99b7 100644 --- a/test/rules/no-empty-named-blocks.spec.js +++ b/test/rules/no-empty-named-blocks.spec.ts @@ -1,11 +1,12 @@ -import { parsers, test } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-empty-named-blocks' -import { RuleTester } from 'eslint' +import { parsers, test } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/no-empty-named-blocks') +const ruleTester = new TSESLint.RuleTester() -function generateSuggestionsTestCases(cases, parser) { +function generateSuggestionsTestCases(cases: string[], parser?: string) { return cases.map(code => test({ code, @@ -29,7 +30,7 @@ function generateSuggestionsTestCases(cases, parser) { } ruleTester.run('no-empty-named-blocks', rule, { - valid: [].concat( + valid: [ test({ code: `import 'mod';` }), test({ code: `import Default from 'mod';` }), test({ code: `import { Named } from 'mod';` }), @@ -37,23 +38,19 @@ ruleTester.run('no-empty-named-blocks', rule, { test({ code: `import * as Namespace from 'mod';` }), // Typescript - parsers.TS - ? [ - test({ code: `import type Default from 'mod';`, parser: parsers.TS }), - test({ - code: `import type { Named } from 'mod';`, - parser: parsers.TS, - }), - test({ - code: `import type Default, { Named } from 'mod';`, - parser: parsers.TS, - }), - test({ - code: `import type * as Namespace from 'mod';`, - parser: parsers.TS, - }), - ] - : [], + test({ code: `import type Default from 'mod';`, parser: parsers.TS }), + test({ + code: `import type { Named } from 'mod';`, + parser: parsers.TS, + }), + test({ + code: `import type Default, { Named } from 'mod';`, + parser: parsers.TS, + }), + test({ + code: `import type * as Namespace from 'mod';`, + parser: parsers.TS, + }), // Flow test({ @@ -86,14 +83,14 @@ ruleTester.run('no-empty-named-blocks', rule, { import { DESCRIPTORS2 } from '../helpers/constants'; `, }), - ), - invalid: [].concat( + ], + invalid: [ test({ code: `import Default, {} from 'mod';`, output: `import Default from 'mod';`, errors: ['Unexpected empty named import block'], }), - generateSuggestionsTestCases([ + ...generateSuggestionsTestCases([ `import {} from 'mod';`, `import{}from'mod';`, `import {} from'mod';`, @@ -101,28 +98,24 @@ ruleTester.run('no-empty-named-blocks', rule, { ]), // Typescript - parsers.TS - ? [].concat( - generateSuggestionsTestCases( - [ - `import type {} from 'mod';`, - `import type {}from 'mod';`, - `import type{}from 'mod';`, - `import type {}from'mod';`, - ], - parsers.TS, - ), - test({ - code: `import type Default, {} from 'mod';`, - output: `import type Default from 'mod';`, - parser: parsers.TS, - errors: ['Unexpected empty named import block'], - }), - ) - : [], + ...generateSuggestionsTestCases( + [ + `import type {} from 'mod';`, + `import type {}from 'mod';`, + `import type{}from 'mod';`, + `import type {}from'mod';`, + ], + parsers.TS, + ), + test({ + code: `import type Default, {} from 'mod';`, + output: `import type Default from 'mod';`, + parser: parsers.TS, + errors: ['Unexpected empty named import block'], + }), // Flow - generateSuggestionsTestCases( + ...generateSuggestionsTestCases( [ `import typeof {} from 'mod';`, `import typeof {}from 'mod';`, @@ -137,5 +130,5 @@ ruleTester.run('no-empty-named-blocks', rule, { parser: parsers.BABEL, errors: ['Unexpected empty named import block'], }), - ), + ], }) diff --git a/test/rules/no-extraneous-dependencies.spec.js b/test/rules/no-extraneous-dependencies.spec.ts similarity index 93% rename from test/rules/no-extraneous-dependencies.spec.js rename to test/rules/no-extraneous-dependencies.spec.ts index 82f621596..b31025c7a 100644 --- a/test/rules/no-extraneous-dependencies.spec.js +++ b/test/rules/no-extraneous-dependencies.spec.ts @@ -1,59 +1,55 @@ -import { parsers, test, testFilePath } from '../utils' -import typescriptConfig from '../../src/config/typescript' import path from 'path' import fs from 'fs' -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-extraneous-dependencies' +import typescriptConfig from '../../src/config/typescript' -const ruleTester = new RuleTester() -const typescriptRuleTester = new RuleTester(typescriptConfig) -const rule = require('rules/no-extraneous-dependencies') +import { + dependencies as deps, + devDependencies as devDeps, +} from '../fixtures/package.json' + +import { parsers, test, testFilePath } from '../utils' + +const ruleTester = new TSESLint.RuleTester() +const typescriptRuleTester = new TSESLint.RuleTester(typescriptConfig) + +const packageDirWithSyntaxError = testFilePath('with-syntax-error') -const packageDirWithSyntaxError = path.join( - __dirname, - '../fixtures/with-syntax-error', -) const packageFileWithSyntaxErrorMessage = (() => { try { JSON.parse( - fs.readFileSync(path.join(packageDirWithSyntaxError, 'package.json')), + fs.readFileSync( + path.join(packageDirWithSyntaxError, 'package.json'), + 'utf8', + ), ) } catch (error) { - return error.message + return (error as Error).message } })() -const packageDirWithFlowTyped = path.join( - __dirname, - '../fixtures/with-flow-typed', -) -const packageDirWithTypescriptDevDependencies = path.join( - __dirname, - '../fixtures/with-typescript-dev-dependencies', + +const packageDirWithFlowTyped = testFilePath('with-flow-typed') +const packageDirWithTypescriptDevDependencies = testFilePath( + 'with-typescript-dev-dependencies', ) -const packageDirMonoRepoRoot = path.join(__dirname, '../fixtures/monorepo') -const packageDirMonoRepoWithNested = path.join( - __dirname, - '../fixtures/monorepo/packages/nested-package', +const packageDirMonoRepoRoot = testFilePath('monorepo') +const packageDirMonoRepoWithNested = testFilePath( + 'monorepo/packages/nested-package', ) -const packageDirWithEmpty = path.join(__dirname, '../fixtures/empty') -const packageDirBundleDeps = path.join( - __dirname, - '../fixtures/bundled-dependencies/as-array-bundle-deps', +const packageDirWithEmpty = testFilePath('empty') +const packageDirBundleDeps = testFilePath( + 'bundled-dependencies/as-array-bundle-deps', ) -const packageDirBundledDepsAsObject = path.join( - __dirname, - '../fixtures/bundled-dependencies/as-object', +const packageDirBundledDepsAsObject = testFilePath( + 'bundled-dependencies/as-object', ) -const packageDirBundledDepsRaceCondition = path.join( - __dirname, - '../fixtures/bundled-dependencies/race-condition', +const packageDirBundledDepsRaceCondition = testFilePath( + 'bundled-dependencies/race-condition', ) -const { - dependencies: deps, - devDependencies: devDeps, -} = require('../fixtures/package.json') - ruleTester.run('no-extraneous-dependencies', rule, { valid: [ ...Object.keys(deps) diff --git a/test/rules/no-import-module-exports.spec.js b/test/rules/no-import-module-exports.spec.ts similarity index 87% rename from test/rules/no-import-module-exports.spec.js rename to test/rules/no-import-module-exports.spec.ts index 488ac44b1..02007bc3f 100644 --- a/test/rules/no-import-module-exports.spec.js +++ b/test/rules/no-import-module-exports.spec.ts @@ -1,20 +1,22 @@ import path from 'path' -import { RuleTester } from 'eslint' -import { eslintVersionSatisfies, test, testVersion } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' -const ruleTester = new RuleTester({ +import rule from '../../src/rules/no-import-module-exports' + +import { test } from '../utils' + +const ruleTester = new TSESLint.RuleTester({ parserOptions: { ecmaVersion: 6, sourceType: 'module' }, }) -const rule = require('rules/no-import-module-exports') const error = { - message: `Cannot use import declarations in modules that export using CommonJS (module.exports = 'foo' or exports.bar = 'hi')`, + messageId: 'notAllowed', type: 'ImportDeclaration', -} +} as const ruleTester.run('no-import-module-exports', rule, { - valid: [].concat( + valid: [ test({ code: ` const thing = require('thing') @@ -39,14 +41,12 @@ ruleTester.run('no-import-module-exports', rule, { exports.foo = bar `, }), - eslintVersionSatisfies('>= 4') - ? test({ - code: ` + test({ + code: ` import { module } from 'qunit' module.skip('A test', function () {}) `, - }) - : [], + }), test({ code: ` import foo from 'path'; @@ -77,7 +77,7 @@ ruleTester.run('no-import-module-exports', rule, { 'test/fixtures/missing-entrypoint/cli.js', ), }), - testVersion('>= 6', () => ({ + test({ code: ` import fs from 'fs/promises'; @@ -126,8 +126,8 @@ ruleTester.run('no-import-module-exports', rule, { parserOptions: { ecmaVersion: 2020, }, - })) || [], - ), + }), + ], invalid: [ test({ code: ` @@ -163,10 +163,7 @@ ruleTester.run('no-import-module-exports', rule, { import foo from 'path'; module.exports = foo; `, - filename: path.join( - process.cwd(), - 'test/fixtures/some/other/entry-point.js', - ), + filename: path.resolve('test/fixtures/some/other/entry-point.js'), options: [{ exceptions: ['**/*/other/file.js'] }], errors: [error], }), diff --git a/test/rules/no-named-export.spec.js b/test/rules/no-named-export.spec.ts similarity index 75% rename from test/rules/no-named-export.spec.js rename to test/rules/no-named-export.spec.ts index 66e428daa..88dca2740 100644 --- a/test/rules/no-named-export.spec.js +++ b/test/rules/no-named-export.spec.ts @@ -1,11 +1,13 @@ -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-named-export' + import { parsers, test, testVersion } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/no-named-export') +const ruleTester = new TSESLint.RuleTester() ruleTester.run('no-named-export', rule, { - valid: [].concat( + valid: [ test({ code: 'export default function bar() {};', }), @@ -29,11 +31,11 @@ ruleTester.run('no-named-export', rule, { }), // es2022: Arbitrary module namespae identifier names - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'let foo; export { foo as "default" }', parserOptions: { ecmaVersion: 2022 }, })), - ), + ], invalid: [ test({ code: ` @@ -43,11 +45,11 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -58,7 +60,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -70,11 +72,11 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -83,7 +85,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -95,7 +97,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -104,7 +106,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -113,7 +115,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -122,7 +124,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -131,7 +133,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -144,11 +146,11 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -157,7 +159,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportAllDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -166,7 +168,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -175,7 +177,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -185,7 +187,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -195,7 +197,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -205,7 +207,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), @@ -215,7 +217,7 @@ ruleTester.run('no-named-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: 'Named exports are not allowed.', + messageId: 'noAllowed', }, ], }), diff --git a/test/rules/no-nodejs-modules.spec.js b/test/rules/no-nodejs-modules.spec.ts similarity index 94% rename from test/rules/no-nodejs-modules.spec.js rename to test/rules/no-nodejs-modules.spec.ts index 33b1a6df5..214d2fc03 100644 --- a/test/rules/no-nodejs-modules.spec.js +++ b/test/rules/no-nodejs-modules.spec.ts @@ -1,16 +1,17 @@ -import { test } from '../utils' -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-nodejs-modules' -const ruleTester = new RuleTester() +import { test } from '../utils' -const rule = require('rules/no-nodejs-modules') +const ruleTester = new TSESLint.RuleTester() -const error = message => ({ +const error = (message: string) => ({ message, }) ruleTester.run('no-nodejs-modules', rule, { - valid: [].concat( + valid: [ test({ code: 'import _ from "lodash"' }), test({ code: 'import find from "lodash.find"' }), test({ code: 'import foo from "./foo"' }), @@ -105,8 +106,8 @@ ruleTester.run('no-nodejs-modules', rule, { }, ], }), - ), - invalid: [].concat( + ], + invalid: [ test({ code: 'import path from "path"', errors: [error('Do not import Node.js builtin module "path"')], @@ -157,5 +158,5 @@ ruleTester.run('no-nodejs-modules', rule, { ], errors: [error('Do not import Node.js builtin module "node:fs"')], }), - ), + ], }) diff --git a/test/rules/no-unassigned-import.spec.js b/test/rules/no-unassigned-import.spec.ts similarity index 91% rename from test/rules/no-unassigned-import.spec.js rename to test/rules/no-unassigned-import.spec.ts index 1b56dfcf9..f1e80a268 100644 --- a/test/rules/no-unassigned-import.spec.js +++ b/test/rules/no-unassigned-import.spec.ts @@ -1,14 +1,16 @@ -import { test } from '../utils' -import * as path from 'path' +import path from 'path' + +import { TSESLint } from '@typescript-eslint/utils' -import { RuleTester } from 'eslint' +import rule from '../../src/rules/no-unassigned-import' -const ruleTester = new RuleTester() -const rule = require('rules/no-unassigned-import') +import { test } from '../utils' + +const ruleTester = new TSESLint.RuleTester() const error = { - message: 'Imported module should be assigned', -} + messageId: 'unassigned', +} as const ruleTester.run('no-unassigned-import', rule, { valid: [ @@ -101,7 +103,7 @@ ruleTester.run('no-unassigned-import', rule, { test({ code: 'import "./styles/app.css"', options: [{ allow: ['styles/*.css'] }], - filename: path.join(process.cwd(), 'src/app.js'), + filename: path.resolve('src/app.js'), errors: [error], }), ], diff --git a/test/rules/no-unresolved.spec.ts b/test/rules/no-unresolved.spec.ts index b0255283f..9a41eaa48 100644 --- a/test/rules/no-unresolved.spec.ts +++ b/test/rules/no-unresolved.spec.ts @@ -11,8 +11,6 @@ import { testVersion, parsers, ValidTestCase, - InvalidTestCaseError, - InvalidTestCase, } from '../utils' const ruleTester = new TSESLint.RuleTester() @@ -20,12 +18,7 @@ const ruleTester = new TSESLint.RuleTester() function runResolverTests(resolver: 'node' | 'webpack') { // redefine 'test' to set a resolver // thus 'rest'. needed something 4-chars-long for formatting simplicity - function rest( - specs: T, - ): T extends { errors: InvalidTestCaseError[] } - ? InvalidTestCase - : ValidTestCase { - // @ts-expect-error -- simplify testing + function rest(specs: T) { return test({ ...specs, settings: { diff --git a/test/rules/no-useless-path-segments.spec.js b/test/rules/no-useless-path-segments.spec.ts similarity index 97% rename from test/rules/no-useless-path-segments.spec.js rename to test/rules/no-useless-path-segments.spec.ts index 73e4a5b2b..c143b53d3 100644 --- a/test/rules/no-useless-path-segments.spec.js +++ b/test/rules/no-useless-path-segments.spec.ts @@ -1,10 +1,12 @@ +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/no-useless-path-segments' + import { parsers, test } from '../utils' -import { RuleTester } from 'eslint' -const ruleTester = new RuleTester() -const rule = require('rules/no-useless-path-segments') +const ruleTester = new TSESLint.RuleTester() -function runResolverTests(resolver) { +function runResolverTests(resolver: 'node' | 'webpack') { ruleTester.run(`no-useless-path-segments (${resolver})`, rule, { valid: [ // CommonJS modules with default options @@ -298,4 +300,4 @@ function runResolverTests(resolver) { }) } -;['node', 'webpack'].forEach(runResolverTests) +;(['node', 'webpack'] as const).forEach(runResolverTests) diff --git a/test/rules/no-webpack-loader-syntax.spec.js b/test/rules/no-webpack-loader-syntax.spec.ts similarity index 68% rename from test/rules/no-webpack-loader-syntax.spec.js rename to test/rules/no-webpack-loader-syntax.spec.ts index 27aa047e5..89baed3fd 100644 --- a/test/rules/no-webpack-loader-syntax.spec.js +++ b/test/rules/no-webpack-loader-syntax.spec.ts @@ -1,10 +1,10 @@ -import { test, parsers } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' -import { RuleTester } from 'eslint' -import semver from 'semver' +import rule from '../../src/rules/no-webpack-loader-syntax' -const ruleTester = new RuleTester() -const rule = require('rules/no-webpack-loader-syntax') +import { test } from '../utils' + +const ruleTester = new TSESLint.RuleTester() const message = 'Do not use import syntax to configure webpack loaders.' @@ -77,34 +77,3 @@ ruleTester.run('no-webpack-loader-syntax', rule, { }), ], }) - -describe('TypeScript', () => { - const parser = parsers.TS - - const parserConfig = { - parser, - settings: { - 'import-x/parsers': { [parser]: ['.ts'] }, - 'import-x/resolver': { 'eslint-import-resolver-typescript': true }, - }, - } - // @typescript-eslint/parser@5+ throw error for invalid module specifiers at parsing time. - // https://github.com/typescript-eslint/typescript-eslint/releases/tag/v5.0.0 - if ( - !( - parser === parsers.TS && - semver.satisfies( - require('@typescript-eslint/parser/package.json').version, - '>= 5', - ) - ) - ) { - ruleTester.run('no-webpack-loader-syntax', rule, { - valid: [ - test({ code: 'import { foo } from\nalert()', ...parserConfig }), - test({ code: 'import foo from\nalert()', ...parserConfig }), - ], - invalid: [], - }) - } -}) diff --git a/test/rules/order.spec.js b/test/rules/order.spec.ts similarity index 98% rename from test/rules/order.spec.js rename to test/rules/order.spec.ts index 282dfdcbd..6987a1cd9 100644 --- a/test/rules/order.spec.js +++ b/test/rules/order.spec.ts @@ -1,26 +1,32 @@ -import { test, parsers, getNonDefaultParsers, testFilePath } from '../utils' - -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' import eslintPkg from 'eslint/package.json' import semver from 'semver' -import { resolve } from 'path' -import babelPresetFlow from '@babel/preset-flow' -const ruleTester = new RuleTester() -const flowRuleTester = new RuleTester({ - parser: resolve(__dirname, '../../node_modules/@babel/eslint-parser'), +import rule from '../../src/rules/order' + +import { + test, + parsers, + getNonDefaultParsers, + testFilePath, + ValidTestCase, +} from '../utils' + +const ruleTester = new TSESLint.RuleTester() + +const flowRuleTester = new TSESLint.RuleTester({ + parser: parsers.BABEL, parserOptions: { requireConfigFile: false, babelOptions: { configFile: false, babelrc: false, - presets: [babelPresetFlow], + presets: ['@babel/flow'], }, }, }) -const rule = require('rules/order') -function withoutAutofixOutput(test) { +function withoutAutofixOutput(test: T) { return { ...test, output: test.code } } @@ -1431,32 +1437,26 @@ ruleTester.run('order', rule, { ], }), // Multiple errors - ...(semver.satisfies(eslintPkg.version, '< 3.0.0') - ? [] - : [ - test({ - code: ` - var sibling = require('./sibling'); - var async = require('async'); - var fs = require('fs'); - `, - output: ` - var async = require('async'); - var sibling = require('./sibling'); - var fs = require('fs'); - `, - errors: [ - { - message: - '`async` import should occur before import of `./sibling`', - }, - { - message: - '`fs` import should occur before import of `./sibling`', - }, - ], - }), - ]), + test({ + code: ` + var sibling = require('./sibling'); + var async = require('async'); + var fs = require('fs'); + `, + output: ` + var async = require('async'); + var sibling = require('./sibling'); + var fs = require('fs'); + `, + errors: [ + { + message: '`async` import should occur before import of `./sibling`', + }, + { + message: '`fs` import should occur before import of `./sibling`', + }, + ], + }), // Uses 'after' wording if it creates less errors test({ code: ` @@ -3010,35 +3010,29 @@ ruleTester.run('order', rule, { ], }), // Alphabetize with require - ...(semver.satisfies(eslintPkg.version, '< 3.0.0') - ? [] - : [ - test({ - code: ` + test({ + code: ` const { cello } = require('./cello'); import { int } from './int'; const blah = require('./blah'); import { hello } from './hello'; `, - output: ` + output: ` import { int } from './int'; const { cello } = require('./cello'); const blah = require('./blah'); import { hello } from './hello'; `, - errors: [ - { - message: - '`./int` import should occur before import of `./cello`', - }, - { - message: - '`./hello` import should occur before import of `./cello`', - }, - ], - }), - ]), - ].filter(Boolean), + errors: [ + { + message: '`./int` import should occur before import of `./cello`', + }, + { + message: '`./hello` import should occur before import of `./cello`', + }, + ], + }), + ], }) describe('TypeScript', () => { @@ -3054,7 +3048,7 @@ describe('TypeScript', () => { } ruleTester.run('order', rule, { - valid: [].concat( + valid: [ // #1667: typescript type import support // Option alphabetize: {order: 'asc'} @@ -3296,8 +3290,8 @@ describe('TypeScript', () => { }, ], }), - ), - invalid: [].concat( + ], + invalid: [ // Option alphabetize: {order: 'asc'} test({ code: ` @@ -3593,7 +3587,7 @@ describe('TypeScript', () => { // { message: '`node:fs/promises` import should occur before import of `express`' }, ], }), - ), + ], }) }) }) diff --git a/test/rules/prefer-default-export.spec.js b/test/rules/prefer-default-export.spec.ts similarity index 87% rename from test/rules/prefer-default-export.spec.js rename to test/rules/prefer-default-export.spec.ts index a3592019d..543f38b64 100644 --- a/test/rules/prefer-default-export.spec.js +++ b/test/rules/prefer-default-export.spec.ts @@ -1,18 +1,14 @@ -import { test, testVersion, getNonDefaultParsers, parsers } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' -import { RuleTester } from 'eslint' +import rule from '../../src/rules/prefer-default-export' -const ruleTester = new RuleTester() -const rule = require('../../src/rules/prefer-default-export') +import { test, testVersion, getNonDefaultParsers, parsers } from '../utils' -const SINGLE_EXPORT_ERROR_MESSAGE = - 'Prefer default export on a file with single export.' -const ANY_EXPORT_ERROR_MESSAGE = - 'Prefer default export to be present on every file that has export.' +const ruleTester = new TSESLint.RuleTester() // test cases for default option { target: 'single' } ruleTester.run('prefer-default-export', rule, { - valid: [].concat( + valid: [ test({ code: ` export const foo = 'foo'; @@ -101,11 +97,11 @@ ruleTester.run('prefer-default-export', rule, { parser: parsers.BABEL, }), // es2022: Arbitrary module namespae identifier names - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'let foo; export { foo as "default" };', parserOptions: { ecmaVersion: 2022 }, })), - ), + ], invalid: [ test({ code: ` @@ -113,7 +109,7 @@ ruleTester.run('prefer-default-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: SINGLE_EXPORT_ERROR_MESSAGE, + messageId: 'single', }, ], }), @@ -123,7 +119,7 @@ ruleTester.run('prefer-default-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: SINGLE_EXPORT_ERROR_MESSAGE, + messageId: 'single', }, ], }), @@ -134,7 +130,7 @@ ruleTester.run('prefer-default-export', rule, { errors: [ { type: 'ExportSpecifier', - message: SINGLE_EXPORT_ERROR_MESSAGE, + messageId: 'single', }, ], }), @@ -144,7 +140,7 @@ ruleTester.run('prefer-default-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: SINGLE_EXPORT_ERROR_MESSAGE, + messageId: 'single', }, ], }), @@ -154,7 +150,7 @@ ruleTester.run('prefer-default-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: SINGLE_EXPORT_ERROR_MESSAGE, + messageId: 'single', }, ], }), @@ -164,7 +160,7 @@ ruleTester.run('prefer-default-export', rule, { errors: [ { type: 'ExportNamedDeclaration', - message: SINGLE_EXPORT_ERROR_MESSAGE, + messageId: 'single', }, ], }), @@ -174,7 +170,7 @@ ruleTester.run('prefer-default-export', rule, { // test cases for { target: 'any' } ruleTester.run('prefer-default-export', rule, { // Any exporting file must contain default export - valid: [].concat( + valid: [ test({ code: ` export default function bar() {};`, @@ -263,7 +259,7 @@ ruleTester.run('prefer-default-export', rule, { ], }), // es2022: Arbitrary module namespae identifier names - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'export const a = 4; let foo; export { foo as "default" };', options: [ { @@ -272,9 +268,9 @@ ruleTester.run('prefer-default-export', rule, { ], parserOptions: { ecmaVersion: 2022 }, })), - ), + ], // { target: 'any' } invalid cases when any exporting file must contain default export but does not - invalid: [].concat( + invalid: [ test({ code: ` export const foo = 'foo'; @@ -286,7 +282,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -301,7 +297,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -316,7 +312,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -332,7 +328,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -346,7 +342,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -361,7 +357,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -375,7 +371,7 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), @@ -389,11 +385,11 @@ ruleTester.run('prefer-default-export', rule, { ], errors: [ { - message: ANY_EXPORT_ERROR_MESSAGE, + messageId: 'any', }, ], }), - ), + ], }) describe('TypeScript', () => { @@ -407,7 +403,7 @@ describe('TypeScript', () => { } ruleTester.run('prefer-default-export', rule, { - valid: [].concat( + valid: [ // Exporting types test({ code: ` @@ -437,7 +433,7 @@ describe('TypeScript', () => { code: `export interface foo { bar: string; }; export function goo() {} /* ${parser.replace(process.cwd(), '$$PWD')}*/`, ...parserConfig, }), - ), + ], invalid: [], }) }) diff --git a/test/rules/unambiguous.spec.js b/test/rules/unambiguous.spec.ts similarity index 87% rename from test/rules/unambiguous.spec.js rename to test/rules/unambiguous.spec.ts index 80bca7571..8765166cd 100644 --- a/test/rules/unambiguous.spec.js +++ b/test/rules/unambiguous.spec.ts @@ -1,8 +1,10 @@ -import { RuleTester } from 'eslint' +import { TSESLint } from '@typescript-eslint/utils' + +import rule from '../../src/rules/unambiguous' + import { parsers } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/unambiguous') +const ruleTester = new TSESLint.RuleTester() ruleTester.run('unambiguous', rule, { valid: [ @@ -52,7 +54,11 @@ ruleTester.run('unambiguous', rule, { code: 'function x() {}', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, output: 'function x() {}', - errors: ['This module could be parsed as a valid script.'], + errors: [ + { + messageId: 'module', + }, + ], }, ], }) diff --git a/test/utils.ts b/test/utils.ts index 4d46daf6e..8cbd79335 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -33,7 +33,7 @@ export function testFilePath(relativePath = 'foo.js') { } export function getNonDefaultParsers() { - return [parsers.TS, parsers.BABEL] + return [parsers.TS, parsers.BABEL] as const } const FILENAME = testFilePath() @@ -44,7 +44,7 @@ export function eslintVersionSatisfies(specifier: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- simplify testing export type ValidTestCase = TSESLint.ValidTestCase & { - errors?: readonly InvalidTestCaseError[] + errors?: readonly InvalidTestCaseError[] | number } export type InvalidTestCase = // eslint-disable-next-line @typescript-eslint/no-explicit-any -- simplify testing @@ -67,13 +67,9 @@ export type InvalidTestCaseError = type?: `${TSESTree.AST_NODE_TYPES}` }) -export function test< - T extends ValidTestCase & { - errors?: readonly InvalidTestCaseError[] - }, ->( +export function test( t: T, -): T extends { errors?: InvalidTestCaseError[] } +): T extends { errors: InvalidTestCaseError[] | number } ? InvalidTestCase : ValidTestCase { if (arguments.length !== 1) { diff --git a/yarn.lock b/yarn.lock index 9dba15e13..64f9bfb80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1802,6 +1802,11 @@ version "0.0.0" uid "" +"@total-typescript/ts-reset@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@total-typescript/ts-reset/-/ts-reset-0.5.1.tgz#93b0535d00faa588518bcfb0db30182e63e4f7af" + integrity sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ== + "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"