Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Commit

Permalink
[new-rule] no-default-import (#4023)
Browse files Browse the repository at this point in the history
* Implement 'no-default-import' rule

* Improve wording in rationale
  • Loading branch information
pablobirukov authored and ericanderson committed Dec 12, 2018
1 parent b9fba26 commit cf35ec8
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const rules = {
"max-file-line-count": [true, 1000],
"max-line-length": [true, 120],
"no-default-export": true,
"no-default-import": true,
"no-duplicate-imports": true,
"no-irregular-whitespace": true,
"no-mergeable-namespace": true,
Expand Down
130 changes: 130 additions & 0 deletions src/rules/noDefaultImportRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @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 RawConfigOptions {
[fromModulesConfigOptionName]: string;
}
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.
The rule supposed to narrow the scope of your changes in the case of monorepo.
Say, you have packages \`A\`, \`B\`, \`C\` and \`utils\`, where \`A\`, \`B\`, \`C\` dependends on \`utils\`,
which is full of default exports.
\`"no-default-export"\` requires you to remove default _export_ from \`utils\`, which leads to changes
in packages \`A\`, \`B\`, \`C\`. It's harder to get merged bigger changeset by various reasons (harder to get your code approved
due to a number of required reviewers; longer build time due to a number of affected packages)
and could result in ignored \`"no-default-export"\` rule in \`utils'\`.
Unlike \`"no-default-export"\`, the rule requires you to repalce default _import_ with named only in \`A\` you work on,
and \`utils\` you import from.`,
optionsDescription: "optionsDescription",
options: {
type: "array",
items: {
type: "object",
properties: {
[fromModulesConfigOptionName]: { type: "string" },
},
required: [
"fromModules",
],
},
},
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 !== undefined && importSpecifier.propertyName.text === "default") {
return importSpecifier.propertyName;
}
}
return null;
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, this.getRuleOptions(this.ruleArguments));
}
private isFromModulesConfigOption(option: boolean | RawConfigOptions): option is RawConfigOptions {
return typeof option === "object" && option[fromModulesConfigOptionName] !== undefined;
}
private getRuleOptions(options: ReadonlyArray<boolean | RawConfigOptions>): Options {
const fromModuleConfigOption = options.find<RawConfigOptions>(this.isFromModulesConfigOption);
if (fromModuleConfigOption !== undefined && typeof fromModuleConfigOption[fromModulesConfigOptionName] === "string") {
return {
[fromModulesConfigOptionName]: new RegExp(fromModuleConfigOption[fromModulesConfigOptionName]),
};
} else {
return {
[fromModulesConfigOptionName]: new RegExp("^\\./|^\\.\\./"),
};
}
}
}

function walk(ctx: Lint.WalkContext<Options>) {
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 !== undefined
&& isStringLiteral(moduleSpecifier)
&& ctx.options[fromModulesConfigOptionName].test(moduleSpecifier.text)
) {
// module name matches specified in rule config
if (importClause.name !== undefined) {
// `import Foo...` syntax
const defaultImportedName = importClause.name;
ctx.addFailureAtNode(defaultImportedName, Rule.FAILURE_STRING);
} else if (importClause.namedBindings !== undefined && isNamedImports(importClause.namedBindings)) {
// `import { default...` syntax
const defaultMember = Rule.getNamedDefaultImport(importClause.namedBindings);
if (defaultMember !== null) {
ctx.addFailureAtNode(defaultMember, Rule.FAILURE_STRING);
}
}
}
}
}
}
36 changes: 36 additions & 0 deletions test/rules/no-default-import/default/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as Utils from "tslint-utils"

import TslintUtils from "tslint-utils"

import Bar, { Foo } from "tslint-misc"

import Bar, * as Foo from "tslint-misc"

import { default as Foo } from "tslint-misc"

import { default as foo, bar } from "tslint-misc"

import { bar, default as foo } from "tslint-misc"

import TslintUtils from "../tslint-utils"
~~~~~~~~~~~ [0]

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
5 changes: 5 additions & 0 deletions test/rules/no-default-import/default/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-default-import": true
}
}
24 changes: 24 additions & 0 deletions test/rules/no-default-import/fromModules/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Utils from "tslint-utils"

import TslintUtils from "../tslint-utils"
~~~~~~~~~~~ [0]

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
10 changes: 10 additions & 0 deletions test/rules/no-default-import/fromModules/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"rules": {
"no-default-import": [
true,
{
"fromModules": "^tslint-|^\\./|^\\.\\./"
}
]
}
}

0 comments on commit cf35ec8

Please sign in to comment.