From 088d69bec1da42410a5120e948be759375d38080 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Mon, 4 Mar 2024 13:40:43 -0700 Subject: [PATCH] Add internal lint rule no-relative-paths-to-internal-packages --- eslint.config.mjs | 2 + .../eslint-plugin-internal/src/rules/index.ts | 2 + .../no-relative-paths-to-internal-packages.ts | 71 +++++++++++ ...elative-paths-to-internal-packages.test.ts | 113 ++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 packages/eslint-plugin-internal/src/rules/no-relative-paths-to-internal-packages.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/no-relative-paths-to-internal-packages.test.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 2c4bedd9b359..620d486967d8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -177,6 +177,8 @@ export default tseslint.config( // '@typescript-eslint/internal/no-poorly-typed-ts-props': 'error', + '@typescript-eslint/internal/no-relative-paths-to-internal-packages': + 'error', '@typescript-eslint/internal/no-typescript-default-import': 'error', '@typescript-eslint/internal/prefer-ast-types-enum': 'error', diff --git a/packages/eslint-plugin-internal/src/rules/index.ts b/packages/eslint-plugin-internal/src/rules/index.ts index c1143e7a6f8d..a99744c4b134 100644 --- a/packages/eslint-plugin-internal/src/rules/index.ts +++ b/packages/eslint-plugin-internal/src/rules/index.ts @@ -1,6 +1,7 @@ import type { Linter } from '@typescript-eslint/utils/ts-eslint'; import noPoorlyTypedTsProps from './no-poorly-typed-ts-props'; +import noRelativePathsToInternalPackages from './no-relative-paths-to-internal-packages'; import noTypescriptDefaultImport from './no-typescript-default-import'; import noTypescriptEstreeImport from './no-typescript-estree-import'; import pluginTestFormatting from './plugin-test-formatting'; @@ -8,6 +9,7 @@ import preferASTTypesEnum from './prefer-ast-types-enum'; export default { 'no-poorly-typed-ts-props': noPoorlyTypedTsProps, + 'no-relative-paths-to-internal-packages': noRelativePathsToInternalPackages, 'no-typescript-default-import': noTypescriptDefaultImport, 'no-typescript-estree-import': noTypescriptEstreeImport, 'plugin-test-formatting': pluginTestFormatting, diff --git a/packages/eslint-plugin-internal/src/rules/no-relative-paths-to-internal-packages.ts b/packages/eslint-plugin-internal/src/rules/no-relative-paths-to-internal-packages.ts new file mode 100644 index 000000000000..3e6153f9b762 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/no-relative-paths-to-internal-packages.ts @@ -0,0 +1,71 @@ +import path from 'path'; + +import { createRule } from '../util'; + +export const REPO_ROOT = path.resolve(__dirname, '../../../..'); +export const PACKAGES_DIR = path.join(REPO_ROOT, 'packages'); + +export default createRule({ + name: __filename, + meta: { + type: 'problem', + docs: { + recommended: 'recommended', + description: 'Disallow relative paths to internal packages', + }, + messages: { + noRelativePathsToInternalPackages: + 'Use absolute paths instead of relative paths to import modules in other internal packages.', + }, + schema: [], + fixable: 'code', + }, + + defaultOptions: [], + + create(context) { + return { + ImportDeclaration(node): void { + const importSource = node.source; + if ( + importSource.value.startsWith('@typescript-eslint') || + !importSource.value.startsWith('.') + ) { + return; + } + + // The idea here is to check if the import source resolves to a different + // package than the package the file is currently in. This lets us not flag + // relative paths to modules inside a package called 'utils' but still flag + // if importing the '@typescript-eslint/utils' package with a relative path. + + const pathOfFileFromPackagesDir = path.relative( + PACKAGES_DIR, + context.physicalFilename, + ); + const packageOfFile = pathOfFileFromPackagesDir.split(path.sep)[0]; + const absolutePathOfImport = path.resolve( + path.dirname(context.physicalFilename), + importSource.value, + ); + const pathOfImportFromPackagesDir = path.relative( + PACKAGES_DIR, + absolutePathOfImport, + ); + const packageOfImport = pathOfImportFromPackagesDir.split(path.sep)[0]; + + if (packageOfImport !== packageOfFile) { + context.report({ + node: importSource, + messageId: 'noRelativePathsToInternalPackages', + fix: fixer => + fixer.replaceText( + importSource, + `'@typescript-eslint/${path.relative(PACKAGES_DIR, absolutePathOfImport)}'`, + ), + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/no-relative-paths-to-internal-packages.test.ts b/packages/eslint-plugin-internal/tests/rules/no-relative-paths-to-internal-packages.test.ts new file mode 100644 index 000000000000..2e1cbf99e216 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/no-relative-paths-to-internal-packages.test.ts @@ -0,0 +1,113 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import path from 'path'; + +import rule, { + PACKAGES_DIR, +} from '../../src/rules/no-relative-paths-to-internal-packages'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-relative-paths-to-internal-packages', rule, { + valid: [ + "import { parse } from '@typescript-eslint/typescript-estree';", + "import { something } from 'not/a/relative/path';", + { + filename: path.resolve( + PACKAGES_DIR, + 'eslint-plugin/src/rules/my-awesome-rule.ts', + ), + code: "import { something } from './utils';", + }, + { + code: "import type { ValueOf } from './utils';", + filename: path.resolve( + PACKAGES_DIR, + 'ast-spec/src/expression/AssignmentExpression/spec.ts', + ), + }, + { + code: "import type { ValueOf } from '../../utils';", + filename: path.resolve( + PACKAGES_DIR, + 'ast-spec/src/expression/AssignmentExpression/spec.ts', + ), + }, + { + code: "import type { ValueOf } from '../../../utils';", + filename: path.resolve( + PACKAGES_DIR, + 'ast-spec/src/expression/AssignmentExpression/spec.ts', + ), + }, + ], + invalid: [ + { + code: "import { parse } from '../../../typescript-estree';", + filename: path.resolve( + PACKAGES_DIR, + 'eslint-plugin/src/rules/my-awesome-rule.ts', + ), + output: `import { parse } from '@typescript-eslint/typescript-estree';`, + errors: [ + { + messageId: 'noRelativePathsToInternalPackages', + line: 1, + }, + ], + }, + { + code: "import { parse } from '../../../typescript-estree/inner-module';", + filename: path.resolve( + PACKAGES_DIR, + 'eslint-plugin/src/rules/my-awesome-rule.ts', + ), + output: `import { parse } from '@typescript-eslint/typescript-estree/inner-module';`, + errors: [ + { + messageId: 'noRelativePathsToInternalPackages', + line: 1, + }, + ], + }, + { + code: "import type { ValueOf } from '../../../../utils';", + output: "import type { ValueOf } from '@typescript-eslint/utils';", + filename: path.resolve( + PACKAGES_DIR, + 'ast-spec/src/expression/AssignmentExpression/spec.ts', + ), + errors: [ + { + messageId: 'noRelativePathsToInternalPackages', + line: 1, + }, + ], + }, + { + code: ` +import type { + MemberExpressionComputedName, + MemberExpressionNonComputedName, +} from '../../../types/src/generated/ast-spec'; + `, + output: ` +import type { + MemberExpressionComputedName, + MemberExpressionNonComputedName, +} from '@typescript-eslint/types/src/generated/ast-spec'; + `, + filename: path.resolve( + PACKAGES_DIR, + 'eslint-plugin/src/rules/prefer-find.ts', + ), + errors: [ + { + messageId: 'noRelativePathsToInternalPackages', + line: 5, + }, + ], + }, + ], +});