From 7da76f626e2f1ec63a0dd36c18d6bb6297bd6cb1 Mon Sep 17 00:00:00 2001 From: JounQin Date: Thu, 14 Mar 2024 00:14:23 +0800 Subject: [PATCH] test: migrate some test cases (#47) --- src/export-map.ts | 5 +- src/index.ts | 3 +- src/rules/default.js | 45 ---------- src/rules/default.ts | 61 +++++++++++++ .../{default.spec.js => default.spec.ts} | 19 ++-- test/rules/{named.spec.js => named.spec.ts} | 90 ++++++++++--------- ...resolved.spec.js => no-unresolved.spec.ts} | 69 +++++++------- test/utils.ts | 43 +++++++-- 8 files changed, 202 insertions(+), 133 deletions(-) delete mode 100644 src/rules/default.js create mode 100644 src/rules/default.ts rename test/rules/{default.spec.js => default.spec.ts} (97%) rename test/rules/{named.spec.js => named.spec.ts} (91%) rename test/rules/{no-unresolved.spec.js => no-unresolved.spec.ts} (93%) diff --git a/src/export-map.ts b/src/export-map.ts index 3899e502c..be5d3f981 100644 --- a/src/export-map.ts +++ b/src/export-map.ts @@ -847,8 +847,11 @@ export class ExportMap { reportErrors( context: RuleContext, - declaration: { source: TSESTree.Literal }, + declaration: { source: TSESTree.Literal | null }, ) { + if (!declaration.source) { + throw new Error('declaration.source is null') + } const msg = this.errors .map(err => `${err.message} (${err.lineNumber}:${err.column})`) .join(', ') diff --git a/src/index.ts b/src/index.ts index 5631f8a75..294b628c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,11 +4,12 @@ import type { PluginConfig } from './types' import noUnresolved from './rules/no-unresolved' import named from './rules/named' +import default_ from './rules/default' export const rules = { 'no-unresolved': noUnresolved, named, - default: require('./rules/default'), + default: default_, namespace: require('./rules/namespace'), 'no-namespace': require('./rules/no-namespace'), export: require('./rules/export'), diff --git a/src/rules/default.js b/src/rules/default.js deleted file mode 100644 index 9ffa9ad92..000000000 --- a/src/rules/default.js +++ /dev/null @@ -1,45 +0,0 @@ -import { ExportMap } from '../export-map' -import { docsUrl } from '../docs-url' - -module.exports = { - meta: { - type: 'problem', - docs: { - category: 'Static analysis', - description: - 'Ensure a default export is present, given a default import.', - url: docsUrl('default'), - }, - schema: [], - }, - - create(context) { - function checkDefault(specifierType, node) { - const defaultSpecifier = node.specifiers.find( - specifier => specifier.type === specifierType, - ) - - if (!defaultSpecifier) { - return - } - const imports = ExportMap.get(node.source.value, context) - if (imports == null) { - return - } - - if (imports.errors.length) { - imports.reportErrors(context, node) - } else if (imports.get('default') === undefined) { - context.report({ - node: defaultSpecifier, - message: `No default export found in imported module "${node.source.value}".`, - }) - } - } - - return { - ImportDeclaration: checkDefault.bind(null, 'ImportDefaultSpecifier'), - ExportNamedDeclaration: checkDefault.bind(null, 'ExportDefaultSpecifier'), - } - }, -} diff --git a/src/rules/default.ts b/src/rules/default.ts new file mode 100644 index 000000000..55c5193d8 --- /dev/null +++ b/src/rules/default.ts @@ -0,0 +1,61 @@ +import { TSESTree } from '@typescript-eslint/utils' +import { ExportMap } from '../export-map' +import { createRule } from '../utils' + +type MessageId = 'noDefaultExport' + +export = createRule<[], MessageId>({ + name: 'default', + meta: { + type: 'problem', + docs: { + category: 'Static analysis', + description: + 'Ensure a default export is present, given a default import.', + recommended: 'warn', + }, + schema: [], + messages: { + noDefaultExport: + 'No default export found in imported module "{{module}}".', + }, + }, + defaultOptions: [], + create(context) { + function checkDefault( + specifierType: 'ImportDefaultSpecifier' | 'ExportDefaultSpecifier', + node: TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration, + ) { + const defaultSpecifier = ( + node.specifiers as Array< + TSESTree.ImportClause | TSESTree.ExportSpecifier + > + ).find(specifier => specifier.type === specifierType) + + if (!defaultSpecifier) { + return + } + const imports = ExportMap.get(node.source!.value, context) + if (imports == null) { + return + } + + if (imports.errors.length) { + imports.reportErrors(context, node) + } else if (imports.get('default') === undefined) { + context.report({ + node: defaultSpecifier, + messageId: 'noDefaultExport', + data: { + module: node.source!.value, + }, + }) + } + } + + return { + ImportDeclaration: checkDefault.bind(null, 'ImportDefaultSpecifier'), + ExportNamedDeclaration: checkDefault.bind(null, 'ExportDefaultSpecifier'), + } + }, +}) diff --git a/test/rules/default.spec.js b/test/rules/default.spec.ts similarity index 97% rename from test/rules/default.spec.js rename to test/rules/default.spec.ts index 3c96cae60..2dfd74fa1 100644 --- a/test/rules/default.spec.js +++ b/test/rules/default.spec.ts @@ -1,14 +1,15 @@ import path from 'path' -import { test, testVersion, SYNTAX_CASES, parsers } from '../utils' -import { RuleTester } from 'eslint' + +import { TSESLint } from '@typescript-eslint/utils' import { CASE_SENSITIVE_FS } from '../../src/utils/resolve' +import rule from '../../src/rules/default' +import { test, testVersion, SYNTAX_CASES, parsers } from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/default') +const ruleTester = new TSESLint.RuleTester() ruleTester.run('default', rule, { - valid: [].concat( + valid: [ test({ code: 'import "./malformed.js"' }), test({ code: 'import foo from "./empty-folder";' }), @@ -96,7 +97,7 @@ ruleTester.run('default', rule, { }), // es2022: Arbitrary module namespace identifier names - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'export { "default" as bar } from "./bar"', parserOptions: { ecmaVersion: 2022, @@ -104,7 +105,7 @@ ruleTester.run('default', rule, { })), ...SYNTAX_CASES, - ), + ], invalid: [ test({ @@ -180,7 +181,7 @@ if (!CASE_SENSITIVE_FS) { describe('TypeScript', () => { const parser = parsers.TS ruleTester.run(`default`, rule, { - valid: [].concat( + valid: [ test({ code: `import foobar from "./typescript-default"`, parser, @@ -285,7 +286,7 @@ describe('TypeScript', () => { 'import-x/resolver': { 'eslint-import-resolver-typescript': true }, }, }), - ), + ], invalid: [ test({ diff --git a/test/rules/named.spec.js b/test/rules/named.spec.ts similarity index 91% rename from test/rules/named.spec.js rename to test/rules/named.spec.ts index 0bc4b6e99..754e067ec 100644 --- a/test/rules/named.spec.js +++ b/test/rules/named.spec.ts @@ -1,3 +1,9 @@ +import path from 'path' + +import { TSESLint, TSESTree } from '@typescript-eslint/utils' + +import { CASE_SENSITIVE_FS } from '../../src/utils/resolve' + import { test, SYNTAX_CASES, @@ -5,15 +11,16 @@ import { testVersion, parsers, } from '../utils' -import { RuleTester } from 'eslint' -import path from 'path' -import { CASE_SENSITIVE_FS } from '../../src/utils/resolve' +import rule from '../../src/rules/named' -const ruleTester = new RuleTester() -const rule = require('rules/named') +const ruleTester = new TSESLint.RuleTester() -function error(name, module, type = 'Identifier') { +function error( + name: string, + module: string, + type: `${TSESTree.AST_NODE_TYPES}` = 'Identifier', +) { return { message: `${name} not found in '${module}'`, type } } @@ -195,40 +202,39 @@ ruleTester.run('named', rule, { ...SYNTAX_CASES, - ...[].concat( - testVersion('>= 6', () => ({ - code: `import { ExtfieldModel, Extfield2Model } from './models';`, - filename: testFilePath('./export-star/downstream.js'), - parserOptions: { - sourceType: 'module', - ecmaVersion: 2020, - }, - })), - - testVersion('>=7.8.0', () => ({ - code: 'const { something } = require("./dynamic-import-in-commonjs")', - parserOptions: { ecmaVersion: 2021 }, - options: [{ commonjs: true }], - })), - - testVersion('>=7.8.0', () => ({ - code: 'import { something } from "./dynamic-import-in-commonjs"', - parserOptions: { ecmaVersion: 2021 }, - })), - - // es2022: Arbitrary module namespace identifier names - testVersion('>= 8.7', () => ({ - code: 'import { "foo" as foo } from "./bar"', - parserOptions: { ecmaVersion: 2022 }, - })), - testVersion('>= 8.7', () => ({ - code: 'import { "foo" as foo } from "./empty-module"', - parserOptions: { ecmaVersion: 2022 }, - })), - ), + ...testVersion('>= 6', () => ({ + code: `import { ExtfieldModel, Extfield2Model } from './models';`, + filename: testFilePath('./export-star/downstream.js'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + }, + })), + + ...testVersion('>=7.8.0', () => ({ + code: 'const { something } = require("./dynamic-import-in-commonjs")', + parserOptions: { ecmaVersion: 2021 }, + options: [{ commonjs: true }], + })), + + ...testVersion('>=7.8.0', () => ({ + code: 'import { something } from "./dynamic-import-in-commonjs"', + parserOptions: { ecmaVersion: 2021 }, + })), + + // es2022: Arbitrary module namespace identifier names + ...testVersion('>= 8.7', () => ({ + code: 'import { "foo" as foo } from "./bar"', + parserOptions: { ecmaVersion: 2022 }, + })), + + ...testVersion('>= 8.7', () => ({ + code: 'import { "foo" as foo } from "./empty-module"', + parserOptions: { ecmaVersion: 2022 }, + })), ], - invalid: [].concat( + invalid: [ test({ code: 'import { somethingElse } from "./test-module"', errors: [error('somethingElse', './test-module')], @@ -368,12 +374,12 @@ ruleTester.run('named', rule, { }), // es2022: Arbitrary module namespace identifier names - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'import { "somethingElse" as somethingElse } from "./test-module"', errors: [error('somethingElse', './test-module', 'Literal')], parserOptions: { ecmaVersion: 2022 }, })), - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'import { "baz" as baz, "bop" as bop } from "./bar"', errors: [ error('baz', './bar', 'Literal'), @@ -381,12 +387,12 @@ ruleTester.run('named', rule, { ], parserOptions: { ecmaVersion: 2022 }, })), - testVersion('>= 8.7', () => ({ + ...testVersion('>= 8.7', () => ({ code: 'import { "default" as barDefault } from "./re-export"', errors: [`default not found in './re-export'`], parserOptions: { ecmaVersion: 2022 }, })), - ), + ], }) // #311: import of mismatched case diff --git a/test/rules/no-unresolved.spec.js b/test/rules/no-unresolved.spec.ts similarity index 93% rename from test/rules/no-unresolved.spec.js rename to test/rules/no-unresolved.spec.ts index 168a0b982..b0255283f 100644 --- a/test/rules/no-unresolved.spec.js +++ b/test/rules/no-unresolved.spec.ts @@ -1,18 +1,31 @@ import path from 'path' -import { test, SYNTAX_CASES, testVersion, parsers } from '../utils' +import { TSESLint } from '@typescript-eslint/utils' import { CASE_SENSITIVE_FS } from '../../src/utils/resolve' +import rule from '../../src/rules/no-unresolved' -import { RuleTester } from 'eslint' +import { + test, + SYNTAX_CASES, + testVersion, + parsers, + ValidTestCase, + InvalidTestCaseError, + InvalidTestCase, +} from '../utils' -const ruleTester = new RuleTester() -const rule = require('rules/no-unresolved') +const ruleTester = new TSESLint.RuleTester() -function runResolverTests(resolver) { +function runResolverTests(resolver: 'node' | 'webpack') { // redefine 'test' to set a resolver // thus 'rest'. needed something 4-chars-long for formatting simplicity - function rest(specs) { + function rest( + specs: T, + ): T extends { errors: InvalidTestCaseError[] } + ? InvalidTestCase + : ValidTestCase { + // @ts-expect-error -- simplify testing return test({ ...specs, settings: { @@ -24,7 +37,7 @@ function runResolverTests(resolver) { } ruleTester.run(`no-unresolved (${resolver})`, rule, { - valid: [].concat( + valid: [ test({ code: 'import "./malformed.js"' }), rest({ code: 'import foo from "./bar";' }), @@ -37,12 +50,12 @@ function runResolverTests(resolver) { }), // check with eslint parser - testVersion('>= 7', () => + ...testVersion('>= 7', () => rest({ code: "import('fs');", parserOptions: { ecmaVersion: 2021 }, }), - ) || [], + ), rest({ code: 'import * as foo from "a"' }), @@ -127,9 +140,9 @@ function runResolverTests(resolver) { code: 'require(foo)', options: [{ commonjs: true }], }), - ), + ], - invalid: [].concat( + invalid: [ rest({ code: 'import reallyfake from "./reallyfake/module"', settings: { 'import-x/ignore': ['^\\./fake/'] }, @@ -199,7 +212,7 @@ function runResolverTests(resolver) { }), // check with eslint parser - testVersion('>= 7', () => + ...testVersion('>= 7', () => rest({ code: "import('in-alternate-root').then(function({DEEP}) {});", errors: [ @@ -210,7 +223,7 @@ function runResolverTests(resolver) { ], parserOptions: { ecmaVersion: 2021 }, }), - ) || [], + ), // export symmetry proposal rest({ @@ -281,7 +294,7 @@ function runResolverTests(resolver) { }, ], }), - ), + ], }) ruleTester.run(`issue #333 (${resolver})`, rule, { @@ -372,7 +385,7 @@ function runResolverTests(resolver) { } } -;['node', 'webpack'].forEach(runResolverTests) +;(['node', 'webpack'] as const).forEach(runResolverTests) ruleTester.run('no-unresolved (import-x/resolve legacy)', rule, { valid: [ @@ -524,21 +537,17 @@ ruleTester.run('no-unresolved syntax verification', rule, { // https://github.com/import-js/eslint-plugin-import-x/issues/2024 ruleTester.run('import() with built-in parser', rule, { - valid: [].concat( - testVersion('>=7', () => ({ - code: "import('fs');", - parserOptions: { ecmaVersion: 2021 }, - })) || [], - ), - invalid: [].concat( - testVersion('>=7', () => ({ - code: 'import("./does-not-exist-l0w9ssmcqy9").then(() => {})', - parserOptions: { ecmaVersion: 2021 }, - errors: [ - "Unable to resolve path to module './does-not-exist-l0w9ssmcqy9'.", - ], - })) || [], - ), + valid: testVersion('>=7', () => ({ + code: "import('fs');", + parserOptions: { ecmaVersion: 2021 }, + })), + invalid: testVersion('>=7', () => ({ + code: 'import("./does-not-exist-l0w9ssmcqy9").then(() => {})', + parserOptions: { ecmaVersion: 2021 }, + errors: [ + "Unable to resolve path to module './does-not-exist-l0w9ssmcqy9'.", + ], + })), }) describe('TypeScript', () => { diff --git a/test/utils.ts b/test/utils.ts index 6aad6967e..4c767f4ad 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,6 +1,6 @@ import path from 'path' -import { TSESLint } from '@typescript-eslint/utils' +import { TSESLint, TSESTree } from '@typescript-eslint/utils' import eslintPkg from 'eslint/package.json' import semver from 'semver' import typescriptPkg from 'typescript/package.json' @@ -41,16 +41,49 @@ export function eslintVersionSatisfies(specifier: string) { return semver.satisfies(eslintPkg.version, specifier) } -type ValidTestCase = TSESLint.ValidTestCase +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- simplify testing +export type ValidTestCase = TSESLint.ValidTestCase & { + errors?: readonly InvalidTestCaseError[] +} -export function testVersion(specifier: string, t: () => ValidTestCase) { - return eslintVersionSatisfies(specifier) ? test(t()) : [] +export type InvalidTestCase = // eslint-disable-next-line @typescript-eslint/no-explicit-any -- simplify testing + TSESLint.InvalidTestCase + +export function testVersion( + specifier: string, + t: () => T, +): T extends { errors: readonly InvalidTestCaseError[] } + ? InvalidTestCase[] + : ValidTestCase[] { + // @ts-expect-error -- simplify testing + return eslintVersionSatisfies(specifier) ? [test(t())] : [] } -export function test(t: ValidTestCase): ValidTestCase { +export type InvalidTestCaseError = + | string + | InvalidTestCase['errors'][number] + | { + type?: `${TSESTree.AST_NODE_TYPES}` + message: string + line?: number + column?: number + endLine?: number + endColumn?: number + } + +export function test< + T extends ValidTestCase & { + errors?: InvalidTestCaseError[] + }, +>( + t: T, +): T extends { errors?: InvalidTestCaseError[] } + ? InvalidTestCase + : ValidTestCase { if (arguments.length !== 1) { throw new SyntaxError('`test` requires exactly one object argument') } + // @ts-expect-error -- simplify testing return { filename: FILENAME, ...t,