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

Commit

Permalink
Add new rule: grouped-imports
Browse files Browse the repository at this point in the history
  • Loading branch information
eriktim committed Jul 21, 2017
1 parent c32bdc4 commit f9c1e3e
Show file tree
Hide file tree
Showing 10 changed files with 183 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 @@ -181,6 +181,7 @@ export const rules = {
// "file-header": No sensible default
"deprecation": true,
"encoding": true,
"grouped-imports": true,
"import-spacing": true,
"interface-name": true,
"interface-over-type-literal": true,
Expand Down
1 change: 1 addition & 0 deletions src/configs/latest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const rules = {
"check-parameters",
],
"no-this-assignment": true,
"grouped-imports": true,
};
// tslint:enable object-literal-sort-keys

Expand Down
152 changes: 152 additions & 0 deletions src/rules/groupedImportsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* @license
* Copyright 2017 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 * as Lint from "tslint";
import { isImportDeclaration } from "tsutils";
import * as ts from "typescript";

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "grouped-imports",
description: "Separate import groups by blank lines.",
rationale: "Keeps a clear overview on dependencies.",
optionsDescription: "Not configurable.",
hasFix: true,
options: {},
optionExamples: [true],
type: "style",
typescriptOnly: false,
};
/* tslint:enable:object-literal-sort-keys */

public static IMPORT_SOURCES_SEPARATED = "Import sources within a group must not be separated by blank lines";
public static IMPORT_SOURCES_NOT_SEPARATED =
"Import sources of different groups must be separated by a single blank line";
public static IMPORT_SOURCES_ORDER =
"Import sources of different groups must be sorted by: libraries, parent directories, current directory";

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new Walker(sourceFile, this.ruleName, this.getOptions()));
}
}

enum ImportStatementType {
LIBRARY_IMPORT = 1,
PARENT_DIRECTORY_IMPORT = 2,
CURRENT_DIRECTORY_IMPORT = 3,
}

interface ImportStatement {
statement: ts.Statement;
type: ImportStatementType;
lineStart: number;
lineEnd: number;
}

class Walker extends Lint.AbstractWalker<Lint.IOptions> {
private lastImportStatement: ImportStatement;

private static getImportStatementType(statement: ts.Statement): ImportStatementType {
const path = Walker.getImportPath(statement);
if (path.charAt(0) === ".") {
if (path.charAt(1) === ".") {
return ImportStatementType.PARENT_DIRECTORY_IMPORT;
} else {
return ImportStatementType.CURRENT_DIRECTORY_IMPORT;
}
} else {
return ImportStatementType.LIBRARY_IMPORT;
}
}

private static getImportPath(statement: ts.Statement): string {
const str = statement.getText();
let index;
let lastIndex;
index = str.indexOf("'");
if (index > 0) {
lastIndex = str.lastIndexOf("'");
} else {
index = str.indexOf("\"");
lastIndex = str.lastIndexOf("\"");
}
if (index < 0 || lastIndex < 0) {
throw new Error(`Unable to extract path from import statement \`${statement.getText()}\``);
}
return str.substring(index + 1, lastIndex);
}

public walk(sourceFile: ts.SourceFile): void {
sourceFile.statements
.filter(isImportDeclaration)
.forEach((st) => this.checkStatement(st));
}

private toImportStatement(statement: ts.Statement): ImportStatement {
return {
lineEnd: this.sourceFile.getLineAndCharacterOfPosition(statement.getEnd()).line,
lineStart: this.sourceFile.getLineAndCharacterOfPosition(statement.getStart()).line,
statement,
type: Walker.getImportStatementType(statement),
};
}

private checkStatement(statement: ts.Statement): void {
const importStatement = this.toImportStatement(statement);
if (this.lastImportStatement) {
this.checkImportStatement(importStatement);
}
this.lastImportStatement = importStatement;
}

private checkImportStatement(importStatement: ImportStatement) {
if (importStatement.type === this.lastImportStatement.type) {
if (importStatement.lineStart !== this.lastImportStatement.lineEnd + 1) {
const replacement = Lint.Replacement.deleteFromTo(
this.lastImportStatement.statement.getEnd() + 1, importStatement.statement.getStart());
this.addFailureAtNode(importStatement.statement, Rule.IMPORT_SOURCES_SEPARATED, replacement);
}
} else if (importStatement.type.valueOf() < this.lastImportStatement.type.valueOf()) {
this.addFailureAtNode(importStatement.statement, Rule.IMPORT_SOURCES_ORDER, this.getAllImportsFix());
} else {
if (importStatement.lineStart !== this.lastImportStatement.lineEnd + 2) {
const replacement = Lint.Replacement.appendText(importStatement.statement.getStart(), ts.sys.newLine);
this.addFailureAtNode(importStatement.statement, Rule.IMPORT_SOURCES_NOT_SEPARATED, replacement);
}
}
}

private getAllImportsFix(): Lint.Fix {
const importStatements = this.sourceFile.statements.filter(isImportDeclaration);
const libs = importStatements.filter((st) => Walker.getImportStatementType(st) === ImportStatementType.LIBRARY_IMPORT);
const parent = importStatements.filter((st) => Walker.getImportStatementType(st) === ImportStatementType.PARENT_DIRECTORY_IMPORT);
const current = importStatements.filter((st) => Walker.getImportStatementType(st) === ImportStatementType.CURRENT_DIRECTORY_IMPORT);
let imports: string[] = [];
[libs, parent, current].forEach((statements) => {
if (statements.length) {
imports = imports.concat(statements.map((st) => st.getText()));
imports.push("");
}
});
return Lint.Replacement.replaceFromTo(
importStatements[0].getStart(),
importStatements[importStatements.length - 1].getEnd(),
imports.join(ts.sys.newLine),
);
}
}
6 changes: 6 additions & 0 deletions test/rules/grouped-imports/bad-order.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {foo} from 'foo';

import {bar} from '../bar';

import './baz';

6 changes: 6 additions & 0 deletions test/rules/grouped-imports/bad-order.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {bar} from '../bar';

import {foo} from 'foo';
~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources of different groups must be sorted by: libraries, parent directories, current directory]

import './baz';
3 changes: 3 additions & 0 deletions test/rules/grouped-imports/different-groups.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {foo} from 'foo';

import {bar} from '../bar';
3 changes: 3 additions & 0 deletions test/rules/grouped-imports/different-groups.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {foo} from 'foo';
import {bar} from '../bar';
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources of different groups must be separated by a single blank line]
2 changes: 2 additions & 0 deletions test/rules/grouped-imports/same-group.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import {foo} from 'foo';
import {bar} from 'bar';
4 changes: 4 additions & 0 deletions test/rules/grouped-imports/same-group.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {foo} from 'foo';

import {bar} from 'bar';
~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must not be separated by blank lines]
5 changes: 5 additions & 0 deletions test/rules/grouped-imports/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"grouped-imports": true
}
}

0 comments on commit f9c1e3e

Please sign in to comment.