Skip to content

Commit 2acc27b

Browse files
rafaelss95wKoza
authored andcommitted
feat(import-destructuring-spacing): add fixer (#595)
1 parent 1ed8d8c commit 2acc27b

File tree

2 files changed

+209
-69
lines changed

2 files changed

+209
-69
lines changed
Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,73 @@
1-
import * as ts from 'typescript';
2-
import * as Lint from 'tslint';
1+
import { Fix, IOptions, IRuleMetadata, Replacement, RuleFailure, Rules, RuleWalker } from 'tslint/lib';
2+
import { NamedImports, SourceFile } from 'typescript/lib/typescript';
33

4-
export class Rule extends Lint.Rules.AbstractRule {
5-
public static metadata: Lint.IRuleMetadata = {
6-
ruleName: 'import-destructuring-spacing',
7-
type: 'style',
4+
export class Rule extends Rules.AbstractRule {
5+
static readonly metadata: IRuleMetadata = {
86
description: 'Ensure consistent and tidy imports.',
9-
rationale: "Imports are easier for the reader to look at when they're tidy.",
7+
hasFix: true,
108
options: null,
119
optionsDescription: 'Not configurable.',
10+
rationale: "Imports are easier for the reader to look at when they're tidy.",
11+
ruleName: 'import-destructuring-spacing',
12+
type: 'style',
1213
typescriptOnly: true
1314
};
1415

15-
public static FAILURE_STRING = "You need to leave whitespaces inside of the import statement's curly braces";
16+
static readonly FAILURE_STRING = "Import statement's curly braces must be spaced exactly by a space to the right and a space to the left";
1617

17-
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
18+
apply(sourceFile: SourceFile): RuleFailure[] {
1819
return this.applyWithWalker(new ImportDestructuringSpacingWalker(sourceFile, this.getOptions()));
1920
}
2021
}
2122

22-
// The walker takes care of all the work.
23-
class ImportDestructuringSpacingWalker extends Lint.RuleWalker {
24-
private scanner: ts.Scanner;
23+
export const getFailureMessage = (): string => {
24+
return Rule.FAILURE_STRING;
25+
};
26+
27+
const isBlankOrMultilineImport = (value: string): boolean => {
28+
return value.indexOf('\n') !== -1 || /^\{\s*\}$/.test(value);
29+
};
2530

26-
constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) {
31+
class ImportDestructuringSpacingWalker extends RuleWalker {
32+
constructor(sourceFile: SourceFile, options: IOptions) {
2733
super(sourceFile, options);
28-
this.scanner = ts.createScanner(ts.ScriptTarget.ES5, false, ts.LanguageVariant.Standard, sourceFile.text);
2934
}
3035

31-
public visitImportDeclaration(node: ts.ImportDeclaration) {
32-
const importClause = node.importClause;
33-
if (importClause && importClause.namedBindings) {
34-
const text = importClause.namedBindings.getText();
36+
protected visitNamedImports(node: NamedImports): void {
37+
this.validateNamedImports(node);
38+
super.visitNamedImports(node);
39+
}
40+
41+
private validateNamedImports(node: NamedImports): void {
42+
const nodeText = node.getText();
3543

36-
if (!this.checkForWhiteSpace(text)) {
37-
this.addFailure(
38-
this.createFailure(importClause.namedBindings.getStart(), importClause.namedBindings.getWidth(), Rule.FAILURE_STRING)
39-
);
40-
}
44+
if (isBlankOrMultilineImport(nodeText)) {
45+
return;
46+
}
47+
48+
const totalLeadingSpaces = nodeText.match(/^\{(\s*)/)[1].length;
49+
const totalTrailingSpaces = nodeText.match(/(\s*)}$/)[1].length;
50+
51+
if (totalLeadingSpaces === 1 && totalTrailingSpaces === 1) {
52+
return;
4153
}
42-
// call the base version of this visitor to actually parse this node
43-
super.visitImportDeclaration(node);
44-
}
4554

46-
private checkForWhiteSpace(text: string) {
47-
if (/\s*\*\s+as\s+[^\s]/.test(text)) {
48-
return true;
55+
const nodeStartPos = node.getStart();
56+
const nodeEndPos = node.getEnd();
57+
let fix: Fix = [];
58+
59+
if (totalLeadingSpaces === 0) {
60+
fix.push(Replacement.appendText(nodeStartPos + 1, ' '));
61+
} else if (totalLeadingSpaces > 1) {
62+
fix.push(Replacement.deleteText(nodeStartPos + 1, totalLeadingSpaces - 1));
63+
}
64+
65+
if (totalTrailingSpaces === 0) {
66+
fix.push(Replacement.appendText(nodeEndPos - 1, ' '));
67+
} else if (totalTrailingSpaces > 1) {
68+
fix.push(Replacement.deleteText(nodeEndPos - totalTrailingSpaces, totalTrailingSpaces - 1));
4969
}
50-
return /{\s[^]*\s}/.test(text);
70+
71+
this.addFailureAtNode(node, getFailureMessage(), fix);
5172
}
5273
}
Lines changed: 158 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,196 @@
1+
import { expect } from 'chai';
2+
import { Replacement } from 'tslint/lib';
3+
import { getFailureMessage, Rule } from '../src/importDestructuringSpacingRule';
14
import { assertAnnotated, assertSuccess } from './testHelper';
25

3-
describe('import-destructuring-spacing', () => {
4-
describe('invalid import spacing', () => {
5-
it('should fail when the imports have no spaces', () => {
6-
let source = `
6+
const {
7+
metadata: { ruleName }
8+
} = Rule;
9+
10+
describe(ruleName, () => {
11+
describe('failure', () => {
12+
it('should fail when a single import has no spaces', () => {
13+
const source = `
714
import {Foo} from './foo'
815
~~~~~
916
`;
1017
assertAnnotated({
11-
ruleName: 'import-destructuring-spacing',
12-
message: "You need to leave whitespaces inside of the import statement's curly braces",
18+
message: getFailureMessage(),
19+
ruleName,
1320
source
1421
});
1522
});
1623

17-
it('should fail with multiple items to import', () => {
18-
let source = `
24+
it('should fail when multiple imports have no spaces', () => {
25+
const source = `
1926
import {Foo,Bar} from './foo'
2027
~~~~~~~~~
2128
`;
22-
assertAnnotated({
23-
ruleName: 'import-destructuring-spacing',
24-
message: "You need to leave whitespaces inside of the import statement's curly braces",
25-
source
26-
});
29+
assertAnnotated({ message: getFailureMessage(), ruleName, source });
2730
});
2831

29-
it('should fail with spaces between items', () => {
30-
let source = `
31-
import {Foo, Bar} from './foo'
32-
~~~~~~~~~~~
33-
`;
34-
assertAnnotated({
35-
ruleName: 'import-destructuring-spacing',
36-
message: "You need to leave whitespaces inside of the import statement's curly braces",
37-
source
38-
});
39-
});
40-
41-
it('should fail with only one whitespace in the left', () => {
42-
let source = `
32+
it('should fail when there are no trailing spaces', () => {
33+
const source = `
4334
import { Foo} from './foo';
4435
~~~~~~
4536
`;
4637
assertAnnotated({
47-
ruleName: 'import-destructuring-spacing',
48-
message: "You need to leave whitespaces inside of the import statement's curly braces",
38+
message: getFailureMessage(),
39+
ruleName,
4940
source
5041
});
5142
});
5243

53-
it('should fail with only one whitespace in the right', () => {
54-
let source = `
44+
it('should fail when there are no leading spaces', () => {
45+
const source = `
5546
import {Foo } from './foo';
5647
~~~~~~
5748
`;
5849
assertAnnotated({
59-
ruleName: 'import-destructuring-spacing',
60-
message: "You need to leave whitespaces inside of the import statement's curly braces",
50+
message: getFailureMessage(),
51+
ruleName,
6152
source
6253
});
6354
});
6455
});
6556

66-
describe('valid import spacing', () => {
57+
describe('failure with replacements', () => {
58+
it('should fail and apply proper replacements when there are no spaces', () => {
59+
const source = `
60+
import {Foo} from './foo';
61+
~~~~~
62+
`;
63+
const failures = assertAnnotated({ message: getFailureMessage(), ruleName, source });
64+
65+
if (!Array.isArray(failures)) {
66+
return;
67+
}
68+
69+
const replacement = Replacement.applyFixes(source, failures.map(f => f.getFix()));
70+
71+
expect(replacement).to.eq(`
72+
import { Foo } from './foo';
73+
~~~~~
74+
`);
75+
});
76+
77+
it('should fail and apply proper replacements when there is more than one leading space', () => {
78+
const source = `
79+
import { Bar, BarFoo, Foo } from './foo';
80+
~~~~~~~~~~~~~~~~~~~~~~~~
81+
`;
82+
const failures = assertAnnotated({ message: getFailureMessage(), ruleName, source });
83+
84+
if (!Array.isArray(failures)) {
85+
return;
86+
}
87+
88+
const replacement = Replacement.applyFixes(source, failures.map(f => f.getFix()));
89+
90+
expect(replacement).to.eq(`
91+
import { Bar, BarFoo, Foo } from './foo';
92+
~~~~~~~~~~~~~~~~~~~~~~~~
93+
`);
94+
});
95+
96+
it('should fail and apply proper replacements when there is more than one trailing space', () => {
97+
const source = `
98+
import { Bar, BarFoo, Foo } from './foo';
99+
~~~~~~~~~~~~~~~~~~~~~~~~
100+
`;
101+
const failures = assertAnnotated({ message: getFailureMessage(), ruleName, source });
102+
103+
if (!Array.isArray(failures)) {
104+
return;
105+
}
106+
107+
const replacement = Replacement.applyFixes(source, failures.map(f => f.getFix()));
108+
109+
expect(replacement).to.eq(`
110+
import { Bar, BarFoo, Foo } from './foo';
111+
~~~~~~~~~~~~~~~~~~~~~~~~
112+
`);
113+
});
114+
115+
it('should fail and apply proper replacements when there is more than one space left and right', () => {
116+
const source = `
117+
import { Bar, BarFoo, Foo } from './foo';
118+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
119+
`;
120+
const failures = assertAnnotated({ message: getFailureMessage(), ruleName, source });
121+
122+
if (!Array.isArray(failures)) {
123+
return;
124+
}
125+
126+
const replacement = Replacement.applyFixes(source, failures.map(f => f.getFix()));
127+
128+
expect(replacement).to.eq(`
129+
import { Bar, BarFoo, Foo } from './foo';
130+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
131+
`);
132+
});
133+
});
134+
135+
describe('success', () => {
67136
it('should succeed with valid spacing', () => {
68-
let source = "import { Foo } from './foo';";
69-
assertSuccess('import-destructuring-spacing', source);
137+
const source = `
138+
import { Foo } from './foo';
139+
`;
140+
assertSuccess(ruleName, source);
141+
});
142+
143+
it('should succeed with multiple spaces between imports', () => {
144+
const source = `
145+
import { Bar, Foo } from './foo';
146+
`;
147+
assertSuccess(ruleName, source);
148+
});
149+
150+
it('should succeed for blank imports', () => {
151+
const source = `
152+
import {} from 'foo';
153+
`;
154+
assertSuccess(ruleName, source);
155+
});
156+
157+
it('should succeed for module imports', () => {
158+
const source = `
159+
import foo = require('./foo');
160+
`;
161+
assertSuccess(ruleName, source);
162+
});
163+
164+
it('should succeed for patch imports', () => {
165+
const source = `
166+
import 'rxjs/add/operator/map';
167+
`;
168+
assertSuccess(ruleName, source);
70169
});
71170

72-
it('should work with alias imports', () => {
73-
let source = "import * as Foo from './foo';";
74-
assertSuccess('import-destructuring-spacing', source);
171+
it('should succeed with alias imports', () => {
172+
const source = `
173+
import * as Foo from './foo';
174+
`;
175+
assertSuccess(ruleName, source);
176+
});
177+
178+
it('should succeed for alias imports inside braces', () => {
179+
const source = `
180+
import { default as _rollupMoment, Moment } from 'moment';
181+
`;
182+
assertSuccess(ruleName, source);
183+
});
184+
185+
it('should succeed for multiline imports', () => {
186+
const source = `
187+
import {
188+
Bar,
189+
BarFoo,
190+
Foo
191+
} from './foo';
192+
`;
193+
assertSuccess(ruleName, source);
75194
});
76195
});
77196
});

0 commit comments

Comments
 (0)