Skip to content

Instantly share code, notes, and snippets.

@danvk
Created June 20, 2016 21:02
Show Gist options
  • Save danvk/8ddaddaf291769b9bcfa8d4504742150 to your computer and use it in GitHub Desktop.
Save danvk/8ddaddaf291769b9bcfa8d4504742150 to your computer and use it in GitHub Desktop.
import * as ts from "typescript";
import * as Lint from "tslint/lib/lint";
export class Rule extends Lint.Rules.AbstractRule {
public static NAMED_IMPORTS_UNORDERED = "Named imports must be alphabetized.";
public static IMPORT_SOURCES_UNORDERED = "Import sources within a block must be alphabetized.";
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const orderedImportsWalker = new OrderedImportsWalker(sourceFile, this.getOptions());
return this.applyWithWalker(orderedImportsWalker);
}
}
// Are the nodes sorted according to the text they contain?
function findUnsortedPair(xs: ts.Node[]): [ts.Node, ts.Node] {
for (let i = 1; i < xs.length; i++) {
if (xs[i].getText().toLowerCase() < xs[i - 1].getText().toLowerCase()) {
return [xs[i - 1], xs[i]];
}
}
return null;
}
// Returns './utils' given the tree for, e.g. "import * as utils from './utils';"
function findFromSource(node: ts.Node): string {
const num = node.getChildCount();
for (let i = 0; i < num - 1; i++) {
const child = node.getChildAt(i);
if (child.kind !== ts.SyntaxKind.FromKeyword) continue;
const next = node.getChildAt(i + 1);
if (next.kind !== ts.SyntaxKind.StringLiteral) {
throw new Error('Unable to parse import: ' + node.getFullText());
}
return next.getText();
}
return null;
}
class OrderedImportsWalker extends Lint.RuleWalker {
// This gets reset after every blank line.
lastImportSource: string = null;
// e.g. "import Foo from './foo';"
public visitImportDeclaration(node: ts.ImportDeclaration) {
const source = findFromSource(node);
if (this.lastImportSource && source < this.lastImportSource) {
this.addFailure(this.createFailure(node.getStart(), node.getWidth(),
Rule.IMPORT_SOURCES_UNORDERED));
}
this.lastImportSource = source;
super.visitImportDeclaration(node);
}
// This is the "{A, B, C}" of "import {A, B, C} from './foo';".
// We need to make sure they're alphabetized.
public visitNamedImports(node: ts.NamedImports) {
const names = node.getChildAt(1); // Three children: "{", "A, B, C" and "}"
const imports: ts.Node[] = [];
for (const child of names.getChildren()) {
if (child.kind === ts.SyntaxKind.ImportSpecifier) {
imports.push(child);
}
}
const pair = findUnsortedPair(imports);
if (pair) {
const [a, b] = pair;
this.addFailure(
this.createFailure(
a.getStart(),
b.getEnd() - a.getStart(),
Rule.NAMED_IMPORTS_UNORDERED));
}
super.visitNamedImports(node);
}
// Check for a blank line, in which case we should reset the import ordering.
public visitNode(node: ts.Node) {
const prefixLength = node.getStart() - node.getFullStart();
const prefix = node.getFullText().slice(0, prefixLength);
if (prefix.indexOf('\n\n') >= 0) {
this.lastImportSource = null;
}
super.visitNode(node);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment