diff --git a/src/rules/import-alias.ts b/src/rules/import-alias.ts index 94c4233..ecc5c4b 100644 --- a/src/rules/import-alias.ts +++ b/src/rules/import-alias.ts @@ -10,6 +10,25 @@ import type { JSONSchema4 } from "json-schema"; import { dirname, join as joinPath, resolve, sep as pathSep } from "path"; import slash from "slash"; +function isPermittedRelativeImport( + importModuleName: string, + relativeImportOverrides: RelativeImportConfig[], + fileAbsoluteDir: string +) { + const configOfPath = relativeImportOverrides.find((config) => + fileAbsoluteDir.includes(resolve(config.path)) + ); + if (!configOfPath) { + return false; + } + + const importParts = importModuleName.split("/"); + const relativeDepth = importParts.filter( + (moduleNamePart) => moduleNamePart === ".." + ).length; + return configOfPath.depth >= relativeDepth; +} + function getAliasSuggestion( importModuleName: string, aliasConfigs: AliasConfig[], @@ -72,10 +91,49 @@ function getBestAliasConfig( : currentBest; }, currentAlias); } + +interface RelativeImportConfig { + /** + * The starting path from which a relative depth is accepted. + * + * @example + * With a configuration like `{ path: "src/foo", depth: 0 }` + * 1. Relative paths can be used in `./src/foo`. + * 2. Relative paths can be used in `./src/foo/bar`. + * 3. Relative paths can NOT be used in `./src`. + * + * @example + * With a configuration like `{ path: "src/*", depth: 0 }` + * 1. Relative paths can be used in `./src/foo`. + * 2. Relative paths can be used in `./src/bar/baz`. + * 3. Relative paths can NOT be used in `./src`. + * + * @example + * With a configuration like `{ path: "src", depth: 0 }` + * 1. Relative paths can be used in `./src/foo`. + * 2. Relative paths can be used in `./src/bar/baz`. + * 3. Relative paths can be used in `./src`. + */ + path: string; + /** + * A positive number which represents the relative depth + * that is acceptable for the associated path. + * + * @example + * In `./src/foo` with `path: "src"` + * 1. `import "./bar"` for `./src/bar` when `depth` \>= `0`. + * 2. `import "./bar/baz"` when `depth` \>= `0`. + * 3. `import "../bar"` when `depth` \>= `1`. + */ + depth: number; +} + type ImportAliasOptions = { aliasConfigPath?: string; // TODO: A fuller solution might need a property for the position, but not sure if needed aliasImportFunctions: string[]; + /** An array defining which paths can be allowed to used relative imports within it to defined depths. */ + relativeImportOverrides?: RelativeImportConfig[]; }; /** @@ -97,6 +155,26 @@ const schemaProperties: Record = { }, default: ["require", "mock"], }, + relativeImportOverrides: { + type: "array", + default: [], + items: { + type: "object", + properties: { + path: { + type: "string", + description: + "The starting path from which a relative depth is accepted.", + }, + depth: { + type: "number", + description: + "A positive number which represents the" + + " relative depth that is acceptable for the associated path.", + }, + }, + }, + }, }; const importAliasRule: Rule.RuleModule = { @@ -125,6 +203,7 @@ const importAliasRule: Rule.RuleModule = { aliasConfigPath, aliasImportFunctions = schemaProperties.aliasImportFunctions .default as string[], + relativeImportOverrides = [], }: ImportAliasOptions = context.options[0] || {}; // No idea what the other array values are const aliasesResult = loadAliasConfigs(cwd, aliasConfigPath); @@ -160,6 +239,16 @@ const importAliasRule: Rule.RuleModule = { // preserve user quote style const quotelessRange: AST.Range = [moduleStart + 1, moduleEnd - 1]; + if ( + isPermittedRelativeImport( + importModuleName, + relativeImportOverrides, + absoluteDir + ) + ) { + return undefined; + } + const aliasSuggestion = getAliasSuggestion( importModuleName, aliasesResult, diff --git a/tests/rules/import-alias.test.ts b/tests/rules/import-alias.test.ts index b58b040..16e877b 100644 --- a/tests/rules/import-alias.test.ts +++ b/tests/rules/import-alias.test.ts @@ -90,6 +90,70 @@ function runTests(platform: "win32" | "posix") { code: `export default TestFn = () => {}`, filename: "src/test.ts", }, + + // relative path overridden for root and exports from sibling module + { + code: `export * from "./rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and exports from sibling module + { + code: `export * from "./rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for root and exports from parent module + { + code: `export * from "../rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 1, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and exports from parent module + { + code: `export * from "../rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + }, ], invalid: [ // more specific alias @@ -120,6 +184,78 @@ function runTests(platform: "win32" | "posix") { filename: "src/test.ts", output: "export * from '#rules/potato';", }, + + // (root) relative export from parent when only depth of 0 (sibling) is allowed + { + code: `export * from "../potato";`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + output: `export * from "#src/potato";`, + }, + + // (specified) relative export from parent when only depth of 0 (sibling) is allowed + { + code: `export * from "../potato";`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + output: `export * from "#src/potato";`, + }, + + // relative path used in file that does not fall within override + { + code: `export * from "./potato";`, + errors: 1, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src/rules", + depth: 0, + }, + ], + }, + ], + output: `export * from "#src/potato";`, + }, + + // relative path used to too large of a depth + { + code: `export * from "../../potato";`, + errors: 1, + filename: "src/rules/foo/bar.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + output: `export * from "#src/potato";`, + }, ], }); @@ -155,6 +291,70 @@ function runTests(platform: "win32" | "posix") { code: `const TestFn = () => {}; export { TestFn };`, filename: "src/test.ts", }, + + // relative path overridden for root and exports from sibling module + { + code: `export { Potato } from "./rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and exports from sibling module + { + code: `export { Potato } from "./rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for root and exports from parent module + { + code: `export { Potato } from "../rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 1, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and exports from parent module + { + code: `export { Potato } from "../rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + }, ], invalid: [ // more specific alias @@ -185,6 +385,78 @@ function runTests(platform: "win32" | "posix") { filename: "src/test.ts", output: "export { Potato } from '#rules/potato';", }, + + // (root) relative export from parent when only depth of 0 (sibling) is allowed + { + code: `export { Potato } from "../potato";`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + output: `export { Potato } from "#src/potato";`, + }, + + // (specified) relative export from parent when only depth of 0 (sibling) is allowed + { + code: `export { Potato } from "../potato";`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + output: `export { Potato } from "#src/potato";`, + }, + + // relative path used in file that does not fall within override + { + code: `export { Potato } from "./potato";`, + errors: 1, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src/rules", + depth: 0, + }, + ], + }, + ], + output: `export { Potato } from "#src/potato";`, + }, + + // relative path used to too large of a depth + { + code: `export { Potato } from "../../potato";`, + errors: 1, + filename: "src/rules/foo/bar.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + output: `export { Potato } from "#src/potato";`, + }, ], }); @@ -210,6 +482,70 @@ function runTests(platform: "win32" | "posix") { code: `import { Potato } from '../src-test/rules/potato';`, filename: "src/test.ts", }, + + // relative path overridden for root and imports from sibling module + { + code: `import { Potato } from "./rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and imports from sibling module + { + code: `import { Potato } from "./rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for root and imports from parent module + { + code: `import { Potato } from "../rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 1, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and imports from parent module + { + code: `import { Potato } from "../rules/potato";`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + }, ], invalid: [ // more specific alias @@ -240,6 +576,78 @@ function runTests(platform: "win32" | "posix") { filename: "src/test.ts", output: "import { Potato } from '#rules/potato';", }, + + // (root) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `import { Potato } from "../potato";`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + output: `import { Potato } from "#src/potato";`, + }, + + // (specified) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `import { Potato } from "../potato";`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + output: `import { Potato } from "#src/potato";`, + }, + + // relative path used in file that does not fall within override + { + code: `import { Potato } from "./potato";`, + errors: 1, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src/rules", + depth: 0, + }, + ], + }, + ], + output: `import { Potato } from "#src/potato";`, + }, + + // relative path used to too large of a depth + { + code: `import { Potato } from "../../potato";`, + errors: 1, + filename: "src/rules/foo/bar.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + output: `import { Potato } from "#src/potato";`, + }, ], }); @@ -266,6 +674,70 @@ function runTests(platform: "win32" | "posix") { code: `require('../src-test/rules/potato')`, filename: "src/test.ts", }, + + // relative path overridden for root and imports from sibling module + { + code: `require("./rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and imports from sibling module + { + code: `require("./rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for root and imports from parent module + { + code: `require("../rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 1, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and imports from parent module + { + code: `require("../rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + }, ], invalid: [ // more specific alias @@ -296,6 +768,77 @@ function runTests(platform: "win32" | "posix") { filename: "src/test.ts", output: `require("#rules/potato")`, }, + // (root) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `require("../potato")`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + output: `require("#src/potato")`, + }, + + // (specified) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `require("../potato")`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + output: `require("#src/potato")`, + }, + + // relative path used in file that does not fall within override + { + code: `require("./potato")`, + errors: 1, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src/rules", + depth: 0, + }, + ], + }, + ], + output: `require("#src/potato")`, + }, + + // relative path used to too large of a depth + { + code: `require("../../potato")`, + errors: 1, + filename: "src/rules/foo/bar.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + output: `require("#src/potato")`, + }, ], }); @@ -321,6 +864,66 @@ function runTests(platform: "win32" | "posix") { code: `jest.mock('../src-test/rules/potato')`, filename: "src/test.ts", }, + // relative path overridden for root and imports from sibling module + { + code: `jest.mock("./rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + }, + // relative path overridden for a specified directory and imports from sibling module + { + code: `jest.mock("./rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + }, + // relative path overridden for root and imports from parent module + { + code: `jest.mock("../rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 1, + }, + ], + }, + ], + }, + // relative path overridden for a specified directory and imports from parent module + { + code: `jest.mock("../rules/potato")`, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + }, ], invalid: [ // more specific alias @@ -351,6 +954,78 @@ function runTests(platform: "win32" | "posix") { filename: "src/test.ts", output: `jest.mock("#rules/potato")`, }, + + // (root) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `jest.mock("../potato")`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + output: `jest.mock("#src/potato")`, + }, + + // (specified) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `jest.mock("../potato")`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + output: `jest.mock("#src/potato")`, + }, + + // relative path used in file that does not fall within override + { + code: `jest.mock("./potato")`, + errors: 1, + filename: "src/test.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src/rules", + depth: 0, + }, + ], + }, + ], + output: `jest.mock("#src/potato")`, + }, + + // relative path used to too large of a depth + { + code: `jest.mock("../../potato")`, + errors: 1, + filename: "src/rules/foo/bar.ts", + options: [ + { + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + output: `jest.mock("#src/potato")`, + }, ], }); @@ -372,17 +1047,85 @@ function runTests(platform: "win32" | "posix") { }, // does not apply for partial path match { - code: `potato('../src-app/rules/potato');`, + code: `potato("../src-app/rules/potato");`, filename: "src/test.ts", options: [{ aliasImportFunctions: ["potato"] }], }, // selects correct alias despite #src being a partial match // and comes first/is shorter as an alias { - code: `potato('../src-test/rules/potato')`, + code: `potato("../src-test/rules/potato")`, filename: "src/test.ts", options: [{ aliasImportFunctions: ["potato"] }], }, + + // relative path overridden for root and imports from sibling module + { + code: `potato("./rules/potato")`, + filename: "src/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and imports from sibling module + { + code: `potato("./rules/potato")`, + filename: "src/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + }, + + // relative path overridden for root and imports from parent module + { + code: `potato("../rules/potato")`, + filename: "src/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: ".", + depth: 1, + }, + ], + }, + ], + }, + + // relative path overridden for a specified directory and imports from parent module + { + code: `potato("../rules/potato")`, + filename: "src/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + }, ], invalid: [ // more specific alias @@ -417,6 +1160,82 @@ function runTests(platform: "win32" | "posix") { options: [{ aliasImportFunctions: ["potato"] }], output: `potato("#rules/potato")`, }, + + // (root) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `potato("../potato")`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: ".", + depth: 0, + }, + ], + }, + ], + output: `potato("#src/potato")`, + }, + + // (specified) relative import from parent when only depth of 0 (sibling) is allowed + { + code: `potato("../potato")`, + errors: 1, + filename: "src/rules/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: "src", + depth: 0, + }, + ], + }, + ], + output: `potato("#src/potato")`, + }, + + // relative path used in file that does not fall within override + { + code: `potato("./potato")`, + errors: 1, + filename: "src/test.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: "src/rules", + depth: 0, + }, + ], + }, + ], + output: `potato("#src/potato")`, + }, + + // relative path used to too large of a depth + { + code: `potato("../../potato")`, + errors: 1, + filename: "src/rules/foo/bar.ts", + options: [ + { + aliasImportFunctions: ["potato"], + relativeImportOverrides: [ + { + path: "src", + depth: 1, + }, + ], + }, + ], + output: `potato("#src/potato")`, + }, ], } );