diff --git a/src/rules/converters/import-blacklist.ts b/src/rules/converters/import-blacklist.ts new file mode 100644 index 000000000..0f0a7f283 --- /dev/null +++ b/src/rules/converters/import-blacklist.ts @@ -0,0 +1,60 @@ +import { RuleConverter } from "../converter"; +import { RequireAtLeastOne } from "../../utils"; + +type ESLintOptionPath = { + name: string; + importNames: string[]; + message?: string; +}; +type ESLintSimpleOption = string[]; +type ESLintComplexOption = RequireAtLeastOne<{ + paths: (string | ESLintOptionPath)[]; + patterns: string[]; +}>; +type ESLintOptions = ESLintSimpleOption | ESLintComplexOption; + +const NOTICE_MATCH_PATTERNS = + "ESLint and TSLint use different strategies to match patterns. TSLint uses standard regular expressions, but ESLint .gitignore spec."; + +export const convertImportBlacklist: RuleConverter = tslintRule => { + let ruleArguments: ESLintOptions = []; + const notices = []; + + if (tslintRule.ruleArguments.every(isString)) { + ruleArguments = tslintRule.ruleArguments; + } else { + const objectOption = tslintRule.ruleArguments.reduce((rules, rule) => { + if (!Array.isArray(rule)) { + const eslintRule = isString(rule) + ? rule + : { + name: Object.keys(rule)[0], + importNames: Object.values(rule)[0] as string[], + }; + return { ...rules, paths: [...(rules.paths || []), eslintRule] }; + } + + return { ...rules, patterns: [...(rules.patterns || []), ...rule] }; + }, {} as ESLintComplexOption); + + if ("patterns" in objectOption && objectOption.patterns.length > 0) { + notices.push(NOTICE_MATCH_PATTERNS); + } + + ruleArguments = [objectOption]; + } + + return { + rules: [ + { + ruleArguments, + ...(notices.length > 0 && { notices }), + ruleName: "no-restricted-imports", + }, + ], + }; +}; + +function isString(value: string): boolean { + return typeof value === "string"; +} diff --git a/src/rules/converters/tests/import-blacklist.test.ts b/src/rules/converters/tests/import-blacklist.test.ts new file mode 100644 index 000000000..7edf8c85e --- /dev/null +++ b/src/rules/converters/tests/import-blacklist.test.ts @@ -0,0 +1,74 @@ +import { convertImportBlacklist } from "../import-blacklist"; + +describe(convertImportBlacklist, () => { + test.each([ + [[], []], + [["rxjs"], ["rxjs"]], + [ + ["rxjs", "lodash"], + ["rxjs", "lodash"], + ], + [ + [".*\\.temp$", ".*\\.tmp$"], + [".*\\.temp$", ".*\\.tmp$"], + ], + [ + ["moment", "date-fns", ["eslint/*"]], + [{ patterns: ["eslint/*"], paths: ["moment", "date-fns"] }], + ], + [ + [{ lodash: ["pullAll", "pull"] }], + [ + { + paths: [ + { + name: "lodash", + importNames: ["pullAll", "pull"], + }, + ], + }, + ], + ], + [ + [ + "rxjs", + [".*\\.temp$", ".*\\.tmp$"], + { lodash: ["pullAll", "pull"] }, + { dummy: ["default"] }, + ], + [ + { + paths: [ + "rxjs", + { + name: "lodash", + importNames: ["pullAll", "pull"], + }, + { + name: "dummy", + importNames: ["default"], + }, + ], + patterns: [".*\\.temp$", ".*\\.tmp$"], + }, + ], + ], + ] as any[][])("convert %j", (ruleArguments: any[], expected: any[]) => { + const result = convertImportBlacklist({ ruleArguments }); + const hasPatterns = typeof expected[0] === "object" && "patterns" in expected[0]; + + expect(result).toEqual({ + rules: [ + { + ruleArguments: expected, + ...(hasPatterns && { + notices: [ + "ESLint and TSLint use different strategies to match patterns. TSLint uses standard regular expressions, but ESLint .gitignore spec.", + ], + }), + ruleName: "no-restricted-imports", + }, + ], + }); + }); +}); diff --git a/src/rules/rulesConverters.ts b/src/rules/rulesConverters.ts index 084e91c22..4ac20ea89 100644 --- a/src/rules/rulesConverters.ts +++ b/src/rules/rulesConverters.ts @@ -18,6 +18,7 @@ import { convertEofline } from "./converters/eofline"; import { convertFileNameCasing } from "./converters/file-name-casing"; import { convertForin } from "./converters/forin"; import { convertFunctionConstructor } from "./converters/function-constructor"; +import { convertImportBlacklist } from "./converters/import-blacklist"; import { convertIncrementDecrement } from "./converters/increment-decrement"; import { convertIndent } from "./converters/indent"; import { convertInterfaceName } from "./converters/interface-name"; @@ -155,6 +156,7 @@ export const rulesConverters = new Map([ ["file-name-casing", convertFileNameCasing], ["forin", convertForin], ["function-constructor", convertFunctionConstructor], + ["import-blacklist", convertImportBlacklist], ["increment-decrement", convertIncrementDecrement], ["indent", convertIndent], ["interface-name", convertInterfaceName], @@ -275,7 +277,6 @@ export const rulesConverters = new Map([ // TSLint core rules: // ["ban", convertBan], // no-restricted-properties - // ["import-blacklist", convertImportBlacklist], // no-restricted-imports // tslint-microsoft-contrib rules: // ["max-func-body-length", convertMaxFuncBodyLength], diff --git a/src/utils.ts b/src/utils.ts index 65139d347..c69d6f9df 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,6 +8,11 @@ export type RemoveErrors = { export type PromiseValue = T extends Promise ? R : never; +export type RequireAtLeastOne = Pick> & + { + [K in Keys]-?: Required> & Partial>>; + }[Keys]; + export const uniqueFromSources = (...sources: (T | T[] | undefined)[]) => { const items: T[] = [];