-
Notifications
You must be signed in to change notification settings - Fork 885
Conversation
Thanks for your interest in palantir/tslint, @trenneman! Before we can accept your pull request, you need to sign our contributor license agreement - just visit https://cla.palantir.com/ and follow the instructions. Once you sign, I'll automatically update this pull request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finished my first review.
The fixer of this rule should not break any code.
Another related question is how comments between imports should be handled? Treated as blank lines, removed while fixing, etc.?
Also CI is failing probably because the existing code in this repo does not conform to this new rule. It would be the best to just disable it in tslint.json
import {bar} from '../bar'; | ||
|
||
import {foo} from 'foo'; | ||
~~~~~~~~~~~~~~~~~~~~~~~~ [Import sources of different groups must be sorted by: libraries, parent directories, current directory] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add a test that has both kinds of error: unnecessary blank line and wrong order
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I only add a single failure & fix in case of wrong orders. I've added a test with both cases though. More on this in a later comment.
src/rules/groupedImportsRule.ts
Outdated
|
||
public walk(sourceFile: ts.SourceFile): void { | ||
sourceFile.statements | ||
.filter(isImportDeclaration) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about ImportEqualsDeclaration
where isExternalModuleReference(statement.moduleReference)
? For example:
import fs = require('fs');
src/rules/groupedImportsRule.ts
Outdated
} | ||
} | ||
|
||
private static getImportPath(statement: ts.Statement): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getImportPath(statement: ts.ImportDeclaration): string {
if (isTextualLiteral(statement.moduleSpecifier)) {
return statement.moduleSpecifier.text;
}
// TODO handle invalid grammar like `import foo from 'a'+foo+'b';`
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added. In what case the isTextualLiteral
returns false? Your example seems to be processed as import foo from 'a'
.
src/rules/groupedImportsRule.ts
Outdated
} | ||
|
||
class Walker extends Lint.AbstractWalker<Lint.IOptions> { | ||
private lastImportStatement: ImportStatement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
: ImportStatement | undefined
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really necessary? It adds a lot of complexity and additional testing throughout the rest of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's just my preference for documentation purposes. It helps someone else (or even you) to understand how the rule works when he needs to change something.
There's no need for any additional checks. In the places where lastImportStatement
can be undefined, you already have a check. In all other places you can simply assert that it's never undefined, e.g. this.lastImportStatement!.type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah great! Didn't know that was possible.
src/rules/groupedImportsRule.ts
Outdated
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this won't work for
import foo from 'foo';import bar from 'bar'; // note the missing space after the first semicolon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, updated the fixes.
src/rules/groupedImportsRule.ts
Outdated
|
||
private checkImportStatement(importStatement: ImportStatement) { | ||
if (importStatement.type === this.lastImportStatement.type) { | ||
if (importStatement.lineStart !== this.lastImportStatement.lineEnd + 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should probably be >
instead of !==
, see comment below
src/rules/groupedImportsRule.ts
Outdated
} 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/!==/</
src/rules/groupedImportsRule.ts
Outdated
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()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the valueOf()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
src/rules/groupedImportsRule.ts
Outdated
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ts.sys.newLine
does not always match the line endings in the current file.
Have a look at eoflineRule.ts
to see how the line ending is inferred there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I've updated this.
src/rules/groupedImportsRule.ts
Outdated
}); | ||
return Lint.Replacement.replaceFromTo( | ||
importStatements[0].getStart(), | ||
importStatements[importStatements.length - 1].getEnd(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that will break code where there's another statement between imports:
import bar from './bar';
let baz = bar.baz;
import foo from 'foo';
Thanks for your first remarks @ajafff. I've fixed some of the issues. But three issues need some further thinking first:
Edit: I've thinking about the comments/additional statements. If you ask me they should all be put AFTER the imports (without deleting any of them). |
965acdf
to
f9b5d0b
Compare
@trenneman
It's probably fine to ignore them like
That would be the best way to go. Maybe someone will complain about the wrong comment position after fixing. But until then we should keep the rule simple. |
@ajafff Please have a look at the updated version. The walker now quits after the first incorrect import and creates a fix for all imports. The imports are removed and re-inserted in the correct order. This ensures no existing code (comments or statements) is lost. |
c44dbb9
to
b19f3ae
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good.
I added some suggestions. And the insertion position for the fix needs to be fixed as mentioned in the comments.
src/rules/groupedImportsRule.ts
Outdated
public walk(sourceFile: ts.SourceFile): void { | ||
const importsStatements = sourceFile.statements | ||
.filter(isImportDeclaration) | ||
.map(this.toImportStatement); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use .map(this.toImportStatement, this)
. Same for .find(this.isBadlyPositioned, this)
below.
That way you can make both methods "real" methods (no need for an arrow function)
src/rules/groupedImportsRule.ts
Outdated
private createFix(importStatements: ImportStatement[]): Lint.Fix { | ||
const newLine = this.getEofChar(this.sourceFile); | ||
const imports = this.getOrderedImports(importStatements); | ||
const addition = Lint.Replacement.appendText(0, imports.join(newLine)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of 0
consider using the start of the first import found as insertion position.
If you use 0, you'll break source files that contain a shebang (e.g. #! env node
) in the first line.
src/rules/groupedImportsRule.ts
Outdated
return char === undefined ? false : Lint.isWhiteSpace(char.charCodeAt(0)); | ||
} | ||
|
||
private getEofChar(sourceFile: ts.SourceFile): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/Eof/Eol/
src/rules/groupedImportsRule.ts
Outdated
if (imps.length == 0) { | ||
return arr; | ||
} | ||
return arr.concat(imps.map((imp) => imp.statement.getText()), ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: consider passing sourceFile
as parameter to .getText()
and .getStart()
for performance reasons.
Though that's not as relevant here, because it's only used in case of a lint error (which should not be the hot path).
* Use newline chars from sourceFile; * Add all imports fix as *only* fix; * Read path using TextualLiteral; * Rename & split checkForFailure; * Remove static methods; * Ignore rule in existing tests; * Add additional tests.
And preserve all statements.
* Stop using arrow function methods; * Insert fix at start of first import statement (and only remove up to the end of the last import statement). * Performance improvements; * Fix eol typo.
b19f3ae
to
83f1960
Compare
Thanks @ajafff. I've processed your comments. I've also rebased on master. Let me know when I should squash as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overall this looks pretty good. I left a few comments; the documentation ones are the most important. also it looks like you need to resolve one file conflict.
what do you think about integrating this behavior into the existing ordered-imports
rule as a new rule option? it does in fact enforce a specific ordering of the groups, so I think it would make sense to users.
pros for integrating into the existing rule:
- In general I would prefer to avoid bloating the core rules list whenever possible. It's already pretty long.
cons:
- Users can't disable this lint rule inline (
// tslint:disable
) independent of other behavior ofordered-imports
. However, I don't think this is a common use case -- users are much more likely to disable the whole rule or rule option for their project. Inline disabling is much more important for rules likemax-line-length
,no-unused-variable
, etc.
also, notes for PR process:
- you don't need to rebase (prefer merging the base branch)
- you don't need to squash (we use github squash merges)
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
ruleName: "grouped-imports", | ||
description: "Separate import groups by blank lines.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs better docs. it's unclear what import groups are from this simple description. I think the phrase "import statement" needs to appear. and the exact algorithm (libraries, parent directories, current directory) needs to be explained in descriptionDetails
of IRuleMetadata
.
hasFix: true, | ||
options: {}, | ||
optionExamples: [true], | ||
type: "style", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would actually argue that this falls into "maintainability"
return false; | ||
} | ||
|
||
private toImportStatement(statement: ts.ImportDeclaration): ImportStatement { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: slightly better name would be importDeclarationToStatement
Thanks @adidahiya I actually think it makes sense to merge it with the |
@trenneman I think opening a new PR would be best (link to it from this one). |
Continued in #3138 . |
PR checklist
Overview of change:
Adds new rule to verify imports are grouped by libraries, parent directories & current directory.
Is there anything you'd like reviewers to focus on?
The fixes for too many/few blank lines fix a single import statement. The fix for changing the order fixes all imports statements completely. I am not sure this is OK.
CHANGELOG.md entry:
[new-rule]
grouped-imports