From ac8079f69eba4a9f6d0fba22bd7c0ac130a69f4d Mon Sep 17 00:00:00 2001 From: Pavel Birukov Date: Mon, 9 Jul 2018 13:03:56 +0300 Subject: [PATCH] Implement 'no-default-import' rule --- src/rules/noDefaultImportRule.ts | 105 ++++++++++++++++++++++ test/rules/no-default-import/test.ts.lint | 21 +++++ test/rules/no-default-import/tslint.json | 7 ++ 3 files changed, 133 insertions(+) create mode 100644 src/rules/noDefaultImportRule.ts create mode 100644 test/rules/no-default-import/test.ts.lint create mode 100644 test/rules/no-default-import/tslint.json diff --git a/src/rules/noDefaultImportRule.ts b/src/rules/noDefaultImportRule.ts new file mode 100644 index 00000000000..958f3f7652d --- /dev/null +++ b/src/rules/noDefaultImportRule.ts @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2016 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isImportDeclaration, isNamedImports, isStringLiteral } from "tsutils"; +import * as ts from "typescript"; +import * as Lint from "../index"; + +const fromModulesConfigOptionName = "fromModules"; +interface Options { + [fromModulesConfigOptionName]: RegExp; +} + +export class Rule extends Lint.Rules.AbstractRule { + /* tslint:disable:object-literal-sort-keys */ + public static metadata: Lint.IRuleMetadata = { + ruleName: "no-default-import", + description: "Disallows importing default members from certain ES6-style modules.", + descriptionDetails: "Import named members instead.", + rationale: Lint.Utils.dedent` + Named imports/exports [promote clarity](https://github.com/palantir/tslint/issues/1182#issue-151780453). + In addition, current tooling differs on the correct way to handle default imports/exports. + Avoiding them all together can help avoid tooling bugs and conflicts. + + While "no-default-import" ???????`, + optionsDescription: "optionsDescription", + options: { + type: "object", + required: [fromModulesConfigOptionName], + properties: { + [fromModulesConfigOptionName]: { type: "string" }, + }, + }, + optionExamples: [ + [true, { [fromModulesConfigOptionName]: "^palantir-|_internal-*" }], + ], + type: "maintainability", + typescriptOnly: false, + }; + /* tslint:enable:object-literal-sort-keys */ + + public static FAILURE_STRING = "Import of default members from this module is forbidden. Import named member instead"; + + public static getNamedDefaultImport(namedBindings: ts.NamedImports): ts.Identifier | null { + for (const importSpecifier of namedBindings.elements) { + if (importSpecifier.propertyName && importSpecifier.propertyName.text === "default") { + return importSpecifier.propertyName; + } + } + return null; + } + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk, this.getRuleOptions()); + } + private getRuleOptions(): Options { + const ruleArguments: Partial = this.ruleArguments[0]; + const fromModules = ruleArguments[fromModulesConfigOptionName]; + return { + [fromModulesConfigOptionName]: new RegExp(fromModules || ""), + }; + } +} + +function walk(ctx: Lint.WalkContext) { + if (ctx.sourceFile.isDeclarationFile || !ts.isExternalModule(ctx.sourceFile)) { + return; + } + for (const statement of ctx.sourceFile.statements) { + if (isImportDeclaration(statement)) { + const { importClause, moduleSpecifier } = statement; + if ( + importClause + && isStringLiteral(moduleSpecifier) + && ctx.options[fromModulesConfigOptionName].test(moduleSpecifier.text) + ) { + // module name matches specified in rule config + if (importClause.name) { + // `import Foo...` syntax + const defaultImportedName = importClause.name; + ctx.addFailureAtNode(defaultImportedName, Rule.FAILURE_STRING); + } else if (importClause.namedBindings && isNamedImports(importClause.namedBindings)) { + // `import { default...` syntax + const defaultMember = Rule.getNamedDefaultImport(importClause.namedBindings); + if (defaultMember) { + ctx.addFailureAtNode(defaultMember, Rule.FAILURE_STRING); + } + } + } + } + } +} diff --git a/test/rules/no-default-import/test.ts.lint b/test/rules/no-default-import/test.ts.lint new file mode 100644 index 00000000000..4a9ab924536 --- /dev/null +++ b/test/rules/no-default-import/test.ts.lint @@ -0,0 +1,21 @@ +import * from "tslint-utils" + +import TslintUtils from "tslint-utils" + ~~~~~~~~~~~ [0] + +import Bar, { Foo } from "tslint-misc" + ~~~ [0] + +import Bar, * as Foo from "tslint-misc" + ~~~ [0] + +import { default as Foo } from "tslint-misc" + ~~~~~~~ [0] + +import { default as foo, bar } from "tslint-misc" + ~~~~~~~ [0] + +import { bar, default as foo } from "tslint-misc" + ~~~~~~~ [0] + +[0]: Import of default members from this module is forbidden. Import named member instead diff --git a/test/rules/no-default-import/tslint.json b/test/rules/no-default-import/tslint.json new file mode 100644 index 00000000000..b84d65ceb8e --- /dev/null +++ b/test/rules/no-default-import/tslint.json @@ -0,0 +1,7 @@ +{ + "rules": { + "no-default-import": [true, { + "fromModules": "^tslint-" + }] + } +} \ No newline at end of file