diff --git a/src/rules/orderedImportsRule.ts b/src/rules/orderedImportsRule.ts index 18bb5e73b28..1bfa5a0c505 100644 --- a/src/rules/orderedImportsRule.ts +++ b/src/rules/orderedImportsRule.ts @@ -21,7 +21,7 @@ import { isImportEqualsDeclaration, isModuleDeclaration, isNamedImports, - isStringLiteral, + isStringLiteral } from "tsutils"; import * as ts from "typescript"; @@ -51,6 +51,7 @@ export class Rule extends Lint.Rules.AbstractRule { Possible values for \`"import-sources-order"\` are: * \`"case-insensitive'\`: Correct order is \`"Bar"\`, \`"baz"\`, \`"Foo"\`. (This is the default.) + * \`"case-insensitive-legacy'\`: Correct order is \`"Bar"\`, \`"baz"\`, \`"Foo"\`. * \`"lowercase-first"\`: Correct order is \`"baz"\`, \`"Bar"\`, \`"Foo"\`. * \`"lowercase-last"\`: Correct order is \`"Bar"\`, \`"Foo"\`, \`"baz"\`. * \`"any"\`: Allow any order. @@ -69,6 +70,7 @@ export class Rule extends Lint.Rules.AbstractRule { Possible values for \`"named-imports-order"\` are: * \`"case-insensitive'\`: Correct order is \`{A, b, C}\`. (This is the default.) + * \`"case-insensitive-legacy'\`: Correct order is \`"Bar"\`, \`"baz"\`, \`"Foo"\`. * \`"lowercase-first"\`: Correct order is \`{b, A, C}\`. * \`"lowercase-last"\`: Correct order is \`{A, C, b}\`. * \`"any"\`: Allow any order. @@ -86,29 +88,47 @@ export class Rule extends Lint.Rules.AbstractRule { type: "object", properties: { "grouped-imports": { - type: "boolean", + type: "boolean" }, "import-sources-order": { type: "string", - enum: ["case-insensitive", "lowercase-first", "lowercase-last", "any"], + enum: [ + "case-insensitive", + "case-insensitive-legacy", + "lowercase-first", + "lowercase-last", + "any" + ] }, "named-imports-order": { type: "string", - enum: ["case-insensitive", "lowercase-first", "lowercase-last", "any"], + enum: [ + "case-insensitive", + "case-insensitive-legacy", + "lowercase-first", + "lowercase-last", + "any" + ] }, "module-source-path": { type: "string", - enum: ["full", "basename"], - }, + enum: ["full", "basename"] + } }, - additionalProperties: false, + additionalProperties: false }, optionExamples: [ true, - [true, {"import-sources-order": "lowercase-last", "named-imports-order": "lowercase-first"}], + [ + true, + { + "import-sources-order": "lowercase-last", + "named-imports-order": "lowercase-first" + } + ] ], type: "style", - typescriptOnly: false, + typescriptOnly: false }; /* tslint:enable:object-literal-sort-keys */ @@ -118,7 +138,9 @@ export class Rule extends Lint.Rules.AbstractRule { public static NAMED_IMPORTS_UNORDERED = "Named imports must be alphabetized."; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new Walker(sourceFile, this.ruleName, parseOptions(this.ruleArguments))); + return this.applyWithWalker( + new Walker(sourceFile, this.ruleName, parseOptions(this.ruleArguments)) + ); } } @@ -127,27 +149,31 @@ export class Rule extends Lint.Rules.AbstractRule { type Transform = (x: string) => string; const TRANSFORMS = new Map([ ["any", () => ""], - ["case-insensitive", (x) => x.toUpperCase()], + ["case-insensitive", x => x.toUpperCase()], + ["case-insensitive-legacy", x => x.toLowerCase()], ["lowercase-first", flipCase], - ["lowercase-last", (x) => x], - ["full", (x) => x], - ["basename", (x) => { - if (!ts.isExternalModuleNameRelative(x)) { - return x; - } + ["lowercase-last", x => x], + ["full", x => x], + [ + "basename", + x => { + if (!ts.isExternalModuleNameRelative(x)) { + return x; + } - const splitIndex = x.lastIndexOf("/"); - if (splitIndex === -1) { - return x; + const splitIndex = x.lastIndexOf("/"); + if (splitIndex === -1) { + return x; + } + return x.substr(splitIndex + 1); } - return x.substr(splitIndex + 1); - }], + ] ]); enum ImportType { LIBRARY_IMPORT = 1, PARENT_DIRECTORY_IMPORT = 2, // starts with "../" - CURRENT_DIRECTORY_IMPORT = 3, // starts with "./" + CURRENT_DIRECTORY_IMPORT = 3 // starts with "./" } interface Options { @@ -170,13 +196,14 @@ function parseOptions(ruleArguments: any[]): Options { "grouped-imports": isGrouped = false, "import-sources-order": sources = "case-insensitive", "named-imports-order": named = "case-insensitive", - "module-source-path": path = "full", - } = optionSet === undefined ? {} : optionSet; + "module-source-path": path = "full" + } = + optionSet === undefined ? {} : optionSet; return { groupedImports: isGrouped, importSourcesOrderTransform: TRANSFORMS.get(sources)!, moduleSourcePath: TRANSFORMS.get(path)!, - namedImportsOrderTransform: TRANSFORMS.get(named)!, + namedImportsOrderTransform: TRANSFORMS.get(named)! }; } @@ -201,8 +228,15 @@ class Walker extends Lint.AbstractWalker { } private checkStatement(statement: ts.Statement): void { - if (!(isImportDeclaration(statement) || isImportEqualsDeclaration(statement)) || - /\r?\n\r?\n/.test(this.sourceFile.text.slice(statement.getFullStart(), statement.getStart(this.sourceFile)))) { + if ( + !(isImportDeclaration(statement) || isImportEqualsDeclaration(statement)) || + /\r?\n\r?\n/.test( + this.sourceFile.text.slice( + statement.getFullStart(), + statement.getStart(this.sourceFile) + ) + ) + ) { this.endBlock(); } @@ -227,11 +261,17 @@ class Walker extends Lint.AbstractWalker { return; } - const source = this.options.importSourcesOrderTransform(removeQuotes(node.moduleSpecifier.text)); + const source = this.options.importSourcesOrderTransform( + removeQuotes(node.moduleSpecifier.text) + ); this.checkSource(source, node); const { importClause } = node; - if (importClause !== undefined && importClause.namedBindings !== undefined && isNamedImports(importClause.namedBindings)) { + if ( + importClause !== undefined && + importClause.namedBindings !== undefined && + isNamedImports(importClause.namedBindings) + ) { this.checkNamedImports(importClause.namedBindings); } } @@ -283,8 +323,9 @@ class Walker extends Lint.AbstractWalker { const pair = findUnsortedPair(imports, this.options.namedImportsOrderTransform); if (pair !== undefined) { const [a, b] = pair; - const sortedDeclarations = sortByKey(imports, (x) => - this.options.namedImportsOrderTransform(x.getText())).map((x) => x.getText()); + const sortedDeclarations = sortByKey(imports, x => + this.options.namedImportsOrderTransform(x.getText()) + ).map(x => x.getText()); // replace in reverse order to preserve earlier offsets for (let i = imports.length - 1; i >= 0; i--) { const start = imports[i].getStart(); @@ -306,7 +347,11 @@ class Walker extends Lint.AbstractWalker { private checkBlockGroups(importsBlock: ImportsBlock): boolean { const oddImportDeclaration = this.getOddImportDeclaration(importsBlock); if (oddImportDeclaration !== undefined) { - this.addFailureAtNode(oddImportDeclaration.node, Rule.IMPORT_SOURCES_NOT_GROUPED, this.getReplacements()); + this.addFailureAtNode( + oddImportDeclaration.node, + Rule.IMPORT_SOURCES_NOT_GROUPED, + this.getReplacements() + ); return true; } return false; @@ -322,22 +367,27 @@ class Walker extends Lint.AbstractWalker { return importDeclarations[0]; } else { this.nextType = type; - return importDeclarations.find((importDeclaration) => importDeclaration.type !== type); + return importDeclarations.find(importDeclaration => importDeclaration.type !== type); } } private getReplacements(): Lint.Replacement[] { const importDeclarationsList = this.importsBlocks - .map((block) => block.getImportDeclarations()) - .filter((imports) => imports.length > 0); + .map(block => block.getImportDeclarations()) + .filter(imports => imports.length > 0); const allImportDeclarations = ([] as ImportDeclaration[]).concat(...importDeclarationsList); const replacements = this.getReplacementsForExistingImports(importDeclarationsList); - const startOffset = allImportDeclarations.length === 0 ? 0 : allImportDeclarations[0].nodeStartOffset; - replacements.push(Lint.Replacement.appendText(startOffset, this.getGroupedImports(allImportDeclarations))); + const startOffset = + allImportDeclarations.length === 0 ? 0 : allImportDeclarations[0].nodeStartOffset; + replacements.push( + Lint.Replacement.appendText(startOffset, this.getGroupedImports(allImportDeclarations)) + ); return replacements; } - private getReplacementsForExistingImports(importDeclarationsList: ImportDeclaration[][]): Lint.Replacement[] { + private getReplacementsForExistingImports( + importDeclarationsList: ImportDeclaration[][] + ): Lint.Replacement[] { return importDeclarationsList.map((items, index) => { let start = items[0].nodeStartOffset; if (index > 0) { @@ -353,12 +403,18 @@ class Walker extends Lint.AbstractWalker { } private getGroupedImports(importDeclarations: ImportDeclaration[]): string { - return [ImportType.LIBRARY_IMPORT, ImportType.PARENT_DIRECTORY_IMPORT, ImportType.CURRENT_DIRECTORY_IMPORT] - .map((type) => { - const imports = importDeclarations.filter((importDeclaration) => importDeclaration.type === type); + return [ + ImportType.LIBRARY_IMPORT, + ImportType.PARENT_DIRECTORY_IMPORT, + ImportType.CURRENT_DIRECTORY_IMPORT + ] + .map(type => { + const imports = importDeclarations.filter( + importDeclaration => importDeclaration.type === type + ); return getSortedImportDeclarationsAsText(imports); }) - .filter((text) => text.length > 0) + .filter(text => text.length > 0) .join(this.getEolChar()); } @@ -378,9 +434,9 @@ class Walker extends Lint.AbstractWalker { interface ImportDeclaration { node: ts.ImportDeclaration | ts.ImportEqualsDeclaration; - nodeEndOffset: number; // end position of node within source file - nodeStartOffset: number; // start position of node within source file - text: string; // initialized with original import text; modified if the named imports are reordered + nodeEndOffset: number; // end position of node within source file + nodeStartOffset: number; // start position of node within source file + text: string; // initialized with original import text; modified if the named imports are reordered sourcePath: string; type: ImportType; } @@ -388,7 +444,11 @@ interface ImportDeclaration { class ImportsBlock { private importDeclarations: ImportDeclaration[] = []; - public addImportDeclaration(sourceFile: ts.SourceFile, node: ImportDeclaration["node"], sourcePath: string) { + public addImportDeclaration( + sourceFile: ts.SourceFile, + node: ImportDeclaration["node"], + sourcePath: string + ) { const start = this.getStartOffset(node); const end = this.getEndOffset(sourceFile, node); const text = sourceFile.text.substring(start, end); @@ -406,7 +466,7 @@ class ImportsBlock { nodeStartOffset: start, sourcePath, text, - type, + type }); } @@ -428,7 +488,8 @@ class ImportsBlock { } const initialText = importDeclaration.text; - importDeclaration.text = initialText.substring(0, start) + replacement + initialText.substring(start + length); + importDeclaration.text = + initialText.substring(0, start) + replacement + initialText.substring(start + length); } public getLastImportSource() { @@ -481,19 +542,24 @@ class ImportsBlock { // Convert aBcD --> AbCd function flipCase(str: string): string { - return Array.from(str).map((char) => { - if (char >= "a" && char <= "z") { - return char.toUpperCase(); - } else if (char >= "A" && char <= "Z") { - return char.toLowerCase(); - } - return char; - }).join(""); + return Array.from(str) + .map(char => { + if (char >= "a" && char <= "z") { + return char.toUpperCase(); + } else if (char >= "A" && char <= "Z") { + return char.toLowerCase(); + } + return char; + }) + .join(""); } // After applying a transformation, are the nodes sorted according to the text they contain? // If not, return the pair of nodes which are out of order. -function findUnsortedPair(xs: ReadonlyArray, transform: (x: string) => string): [ts.Node, ts.Node] | undefined { +function findUnsortedPair( + xs: ReadonlyArray, + transform: (x: string) => string +): [ts.Node, ts.Node] | undefined { for (let i = 1; i < xs.length; i++) { if (transform(xs[i].getText()) < transform(xs[i - 1].getText())) { return [xs[i - 1], xs[i]]; @@ -520,15 +586,15 @@ function compare(a: string, b: string): 0 | 1 | -1 { function removeQuotes(value: string): string { // strip out quotes - if (value.length > 1 && (value[0] === "'" || value[0] === "\"")) { + if (value.length > 1 && (value[0] === "'" || value[0] === '"')) { value = value.substr(1, value.length - 2); } return value; } function getSortedImportDeclarationsAsText(importDeclarations: ImportDeclaration[]): string { - const sortedDeclarations = sortByKey(importDeclarations.slice(), (x) => x.sourcePath); - return sortedDeclarations.map((x) => x.text).join(""); + const sortedDeclarations = sortByKey(importDeclarations.slice(), x => x.sourcePath); + return sortedDeclarations.map(x => x.text).join(""); } function sortByKey(xs: ReadonlyArray, getSortKey: (x: T) => string): T[] { diff --git a/test/rules/ordered-imports/case-insensitive-legacy/test.ts.fix b/test/rules/ordered-imports/case-insensitive-legacy/test.ts.fix new file mode 100644 index 00000000000..31b9a3fddf0 --- /dev/null +++ b/test/rules/ordered-imports/case-insensitive-legacy/test.ts.fix @@ -0,0 +1,48 @@ +// Named imports should be alphabetized. +import {A, B} from 'foo'; +import {A, B} from 'foo'; // failure + +// Case is irrelevant for named import ordering. +import {A, bz, C} from 'foo'; // failure +import {A, b, C} from 'zfoo'; + +// Underscores come first. +import {_b, A, C, d} from 'zfoo'; // failure +import {_b, A, C, d} from 'zfoo'; + +import {g} from "y"; // failure +import { + a as d, + b as c, +} from "z"; + +// Import sources should be alphabetized. +import * as bar from 'bar'; +import * as foo from 'foo'; + +import * as abc from 'abc'; +import * as bar from 'bar'; // failure +import * as foo from 'foo'; + +// ignore quotes +import * as bar from 'bar'; +import * as foo from "foo"; + +import * as bar from "bar"; +import * as foo from 'foo'; + +// Case is irrelevant for source import ordering. +import {A, B} from 'Bar'; +import {A, B} from 'baz'; +import {A, B} from 'Foo'; // should not fail + +// Other styles of import statements. +import someDefault from "module"; +import "something"; +import someDefault, {nameA, nameBReallyLong as anotherName} from "./wherever"; + +// do not fix cases where a newline is missing +import * as foo from 'foo'; import * as bar from 'bar'; + +import * as bar from 'bar'; +import * as foo from 'foo'; diff --git a/test/rules/ordered-imports/case-insensitive-legacy/test.ts.lint b/test/rules/ordered-imports/case-insensitive-legacy/test.ts.lint new file mode 100644 index 00000000000..1ab7bbe5114 --- /dev/null +++ b/test/rules/ordered-imports/case-insensitive-legacy/test.ts.lint @@ -0,0 +1,61 @@ +// Named imports should be alphabetized. +import {A, B} from 'foo'; +import {B, A} from 'foo'; // failure + ~~~~ [ordered-imports] + +// Case is irrelevant for named import ordering. +import {A, b, C} from 'zfoo'; +import {bz, A, C} from 'foo'; // failure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must be alphabetized.] + ~~~~~ [Named imports must be alphabetized.] + +// Underscores come first. +import {A, _b, C, d} from 'zfoo'; // failure + ~~~~~ [Named imports must be alphabetized.] +import {_b, A, C, d} from 'zfoo'; + +import { + b as c, + ~~~~~~~ + a as d, +~~~~~~~~~~ [Named imports must be alphabetized.] +} from "z"; +import {g} from "y"; // failure +~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must be alphabetized.] + +// Import sources should be alphabetized. +import * as bar from 'bar'; +import * as foo from 'foo'; + +import * as abc from 'abc'; +import * as foo from 'foo'; +import * as bar from 'bar'; // failure +~~~~~~~~~~~~~~~~~~~~~~~~~~~ [ordered-sources] + +// ignore quotes +import * as bar from 'bar'; +import * as foo from "foo"; + +import * as bar from "bar"; +import * as foo from 'foo'; + +// Case is irrelevant for source import ordering. +import {A, B} from 'Bar'; +import {A, B} from 'baz'; +import {A, B} from 'Foo'; // should not fail + +// Other styles of import statements. +import someDefault, {nameA, nameBReallyLong as anotherName} from "./wherever"; +import someDefault from "module"; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must be alphabetized.] +import "something"; + +// do not fix cases where a newline is missing +import * as foo from 'foo'; import * as bar from 'bar'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must be alphabetized.] + +import * as foo from 'foo'; +import * as bar from 'bar'; +~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must be alphabetized.] +[ordered-imports]: Named imports must be alphabetized. +[ordered-sources]: Import sources within a group must be alphabetized. diff --git a/test/rules/ordered-imports/case-insensitive-legacy/tslint.json b/test/rules/ordered-imports/case-insensitive-legacy/tslint.json new file mode 100644 index 00000000000..611601029ac --- /dev/null +++ b/test/rules/ordered-imports/case-insensitive-legacy/tslint.json @@ -0,0 +1,11 @@ +{ + "rules": { + "ordered-imports": [ + true, + { + "import-sources-order": "case-insensitive-legacy", + "named-imports-order": "case-insensitive-legacy" + } + ] + } +} diff --git a/test/rules/ordered-imports/case-insensitive/test.ts.fix b/test/rules/ordered-imports/case-insensitive/test.ts.fix index 2bbfa13056a..11a5c889203 100644 --- a/test/rules/ordered-imports/case-insensitive/test.ts.fix +++ b/test/rules/ordered-imports/case-insensitive/test.ts.fix @@ -7,8 +7,8 @@ import {A, bz, C} from 'foo'; // failure import {A, b, C} from 'zfoo'; // Underscores come last. -import {A, C, _b} from 'zfoo'; // failure -import {A, C, _b} from 'zfoo'; +import {A, C, d, _b} from 'zfoo'; // failure +import {A, C, d, _b} from 'zfoo'; import {g} from "y"; // failure import { @@ -44,5 +44,5 @@ import someDefault, {nameA, nameBReallyLong as anotherName} from "./wherever"; // do not fix cases where a newline is missing import * as foo from 'foo'; import * as bar from 'bar'; +import * as bar from 'bar'; import * as foo from 'foo'; -import * as bar from 'bar'; \ No newline at end of file diff --git a/test/rules/ordered-imports/case-insensitive/test.ts.lint b/test/rules/ordered-imports/case-insensitive/test.ts.lint index b2f83c9ccb1..ee69fe602f0 100644 --- a/test/rules/ordered-imports/case-insensitive/test.ts.lint +++ b/test/rules/ordered-imports/case-insensitive/test.ts.lint @@ -10,9 +10,9 @@ import {bz, A, C} from 'foo'; // failure ~~~~~ [Named imports must be alphabetized.] // Underscores come last. -import {A, _b, C} from 'zfoo'; // failure +import {A, _b, C, d} from 'zfoo'; // failure ~~~~~ [Named imports must be alphabetized.] -import {A, C, _b} from 'zfoo'; +import {A, C, d, _b} from 'zfoo'; import { b as c, @@ -58,4 +58,4 @@ import * as foo from 'foo'; import * as bar from 'bar'; ~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources within a group must be alphabetized.] [ordered-imports]: Named imports must be alphabetized. -[ordered-sources]: Import sources within a group must be alphabetized. \ No newline at end of file +[ordered-sources]: Import sources within a group must be alphabetized.