-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce an organizeImports command #21909
Conversation
This will need to be reconciled with #21876. |
src/services/services.ts
Outdated
} | ||
} | ||
|
||
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => { |
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 comparison should be shared with #21876.
src/services/services.ts
Outdated
if (isImportDeclaration(node)) { | ||
oldImportDecls.push(node); | ||
} | ||
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule) |
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.
Oops, forgot to mention this other TODO in the description.
I don't understand the build failures. I ran |
|
I think our mistake is that windows shells don't expand globbing patterns natively? Probably? I'm not sure. |
} | ||
} | ||
|
||
function assertListEqual(list1: ReadonlyArray<Node>, list2: ReadonlyArray<Node>) { |
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 think it would be simpler to emit and compare strings, vs parsing and comparing ASTs. That way there's no need to write assertEqual
and we also test if we are correctly formatting the result. Also would make error messages more readable.
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.
Suits me. How do I stringify them?
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'm attempting to do this, but the emitter works best if the ast it's working on is either entirely synthesized, or not at all. I'm debugging and it looks like there are some nodes with pos
set as children of a synthesized node, which causes the emitter to look at the source file for comments, only there is no source text. Some part of organizeImports might be forgetting to clone a node?
On this subject, we should have fourslash tests of this feature to ensure we're testing the whole thing, including emit.
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.
Regarding testing with text, it seems not to have been simpler. I'm going to leave things as they are until we have a concrete problem to correct or improvement to make.
I was under the impression that the baseline tests were doing an end-to-end test. Did you mean something else?
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.
No, baseline tests will do.
@@ -1907,6 +1907,58 @@ namespace ts { | |||
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext }); | |||
} | |||
|
|||
function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> { |
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 think these changes are substantial enough to deserve an organizeImports.ts
file, just like we have for breakpoints/classifier/etc.
assertListEqual(expectedCoalescedImports, actualCoalescedImports); | ||
}); | ||
|
||
it("Combine namespace imports", () => { |
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.
Name is "Combine namespace imports" but we're actually testing that we don't combine them
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.
The name describes the action taken, rather than the outcome. Combining namespace imports happens to be a no-op.
}); | ||
|
||
it("Combine namespace import with default import", () => { | ||
const sortedImports = parseImports( |
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 pattern occurs a lot, could there be a helper so this can be written:
assertCoalesce([`import * as x from "lib";`, `import y from "lib";`], `import y, * as x from "lib";`)
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 left it expanded because I thought it made the test more readable. A list of unnamed strings can be hard to follow.
assert.equal(testPath, changes[0].fileName); | ||
|
||
Harness.Baseline.runBaseline(baselinePath, () => { | ||
const data: 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.
Since the things we push to data
are the same, this could just be an array literal ["// ==ORIGINAL==", testContent, "// ==ORGANIZED==", textChanges.applyChanges(testContent, changes[0].textChanges)].join(newLineCharacter)
.
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.
Frankly, I didn't understand why we didn't just build a string directly. Is the array literal preferable?
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.
Well, it lets you avoid writing ${newLineCharacter}
a lot... though that's hardcoded to \n
anyway, so you could just write \n
.
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.
In older JS runtimes before everyone switched to ropes, repeated string concat was expensive. string[]
-> one string
via join
is basically the equivalent of using a StringBuilder
in C#
src/services/organizeImports.ts
Outdated
cancellationToken: CancellationToken) { | ||
|
||
// All of the (old) ImportDeclarations in the file, in syntactic order. | ||
const oldImportDecls: ImportDeclaration[] = []; |
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.
const oldImportDecls: ReadonlyArray<ImportDeclaration> = sourceFile.statements.filter(isImportDeclaration);
-- should be fast enough that you don't need to check cancellationToken inside the filter.
Actually, I would just remove cancellationToken
unless we actually see a perf problem...
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.
You could also filter out invalid imports here, and be able to assume that getExternalModuleName
returns a defined result everywhere else.
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.
Fair. It was mostly there so I could test the wait dialog behavior.
src/services/organizeImports.ts
Outdated
cancellationToken.throwIfCancellationRequested(); | ||
|
||
// All of the (new) ImportDeclarations in the file, in sorted order. | ||
const newImportDecls = coalescedImportDecls; |
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.
Just name coalescedImportDecls
newImportDecls
?
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.
Are locals expensive in TS? I like naming things - it makes code easier to read.
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'm sure modern JS engines know that these are the same thing. I normally agree that giving a descriptive name for a variable is useful, but const coalescedImportDecls = coalesceImports(sortedImportDecls);
just repeats the same information in two different names, since the function name already tells you what it's doing -- same for the above 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.
In the debugger, it's easy to inspect each stage individually. If you'd prefer not to have any redundant text, arguably they should be combined into a single expression with only final value stored in a named variable.
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 pure functions, you can enter e.g. coalesceImports(sortedImportDecls)
into the watch list for inspection without needing a named local variable.
src/services/organizeImports.ts
Outdated
} | ||
else { | ||
// Delete the surrounding trivia because it will have been retained in newImportDecls. | ||
const replaceOptions = { |
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.
Inline replaceOptions
, that way we get a contextual type and can warn on misnamed properties.
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.
Sure.
src/services/organizeImports.ts
Outdated
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions); | ||
} | ||
|
||
const changes = changeTracker.getChanges(); |
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.
return changeTracker.getChanges();
(remember the debugger will let you see the return value even if it is not named.)
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.
Sure
src/services/organizeImports.ts
Outdated
} | ||
|
||
function getExternalModuleName(specifier: Expression) { | ||
return isStringLiteral(specifier) || isNoSubstitutionTemplateLiteral(specifier) |
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.
#21953 would be useful here.
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!
src/services/organizeImports.ts
Outdated
|
||
/* @internal */ // Internal for testing | ||
export function sortImports(oldImports: ReadonlyArray<ImportDeclaration>) { | ||
if (oldImports.length < 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.
- In
core.ts
:
export function compareBooleans(a: boolean, b: boolean): Comparison {
return compareValues(a ? 1 : 0, b ? 1 : 0);
}
- Perform
length < 2
optimization insidestableSort
- Here:
return stableSort(oldImports, ({ moduleSpecifier: m1 }, { moduleSpecifier: m2 }) => {
const name1 = getExternalModuleName(m1) || m1.getText();
const name2 = getExternalModuleName(m2) || m2.getText();
return compareBooleans(isExternalModuleNameRelative(name1), isExternalModuleNameRelative(name2)) || compareStringsCaseSensitive(name1, name2);
});
This would break the non-relative vs invalid
and relative vs invalid
tests, but I don't think it really matters how we sort invalid expressions in import positions.
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'm not sure I understand your concerns about the current implementation.
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.
It's not that I spotted any bugs, just noticed that it could be done in 4 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.
It hadn't occurred to me to combine Comparison
s with ||
- applying that should clean things up a bit.
One of the reasons I pulled out the struct was so that the same work wouldn't be repeated for every comparison in which a given import participated. If that's not interesting, it can obviously be simplified.
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.
If you filter out invalid imports at the top, getExternalModuleName
is just a property lookup. isExternalModuleNameRelative
isn't very complicated either -- some simple regex and indexOf
tests.
src/services/organizeImports.ts
Outdated
return []; | ||
} | ||
|
||
const usedImportDecls = removeUnusedImports(oldImportDecls); |
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.
Probably too many calls to throwIfCancellationRequested
here - in general you only need to call this every ~50-200ms, and I don't think sortImports
or coalesceImports
could ever be that slow.
* @param sortedImports a list of ImportDeclarations, sorted by module name. | ||
*/ | ||
export function coalesceImports(sortedImports: ReadonlyArray<ImportDeclaration>) { | ||
if (sortedImports.length === 0) { |
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.
You already tested for length at the top of organizeImports
, and sorting wouldn't have changed the length.
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 believe this also runs after unused import removal.
* @param sortedImports a non-empty list of ImportDeclarations, sorted by module name. | ||
*/ | ||
function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> { | ||
Debug.assert(length(sortedImports) > 0); |
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.
function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
return groupBy(sortedImports, i => getExternalModuleName(i.moduleSpecifier));
}
function groupBy<T>(values: ReadonlyArray<T>, by: (value: T) => string): ReadonlyArray<ReadonlyArray<T>> {
Debug.assert(values.length !== 0);
const groups: T[][] = [];
let groupName = by(values[0]);
let group: T[] = [];
for (const value of values) {
const b = by(value);
if (b === groupName) {
group.push(value);
}
else {
if (group.length) {
groups.push(group);
}
groupName = b;
group = [value];
}
}
if (group.length) {
groups.push(group);
}
return groups;
}
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.
Maybe better:
function groupSortedImports(sortedImports: ReadonlyArray<ImportDeclaration>): ReadonlyArray<ReadonlyArray<ImportDeclaration>> {
return groupBy(sortedImports, (a, b) => getExternalModuleName(a.moduleSpecifier) === getExternalModuleName(b.moduleSpecifier));
}
function groupBy<T>(values: ReadonlyArray<T>, areSameGroup: (a: T, b: T) => boolean): ReadonlyArray<ReadonlyArray<T>> {
Debug.assert(values.length !== 0);
const groups: T[][] = [];
let group: T[] = [];
for (const value of values) {
if (group.length && !areSameGroup(value, group[0])) {
groups.push(group);
group = [];
}
group.push(value);
}
if (group.length) {
groups.push(group);
}
return groups;
}
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.
Just to confirm, your improvement was pulling out a generic helper? The actual behavior is the same?
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.
Yes, assuming invalid imports are filtered out (so getExternalModuleName
returns a defined result).
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.
Just noticed we don't need Debug.assert(values.length !== 0);
in the "maybe better" version.
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.
Note that this isn't true group-by - it requires that the input be sorted. Is it still worth pulling out a generic helper for specialized functionality?
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.
Actually, this doesn't just rely on it being sorted, but on it being sorted by the same mechanism as areSameGroup
. Maybe it would be better to group first (not relying on sorting) and then sort the groups?
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.
Yes, that's why it's a private helper function. At the moment, there doesn't seem to be a need for something more general, does 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.
As you requested, we are now grouping before performing the other operations.
src/services/organizeImports.ts
Outdated
cancellationToken: CancellationToken) { | ||
|
||
// All of the (old) ImportDeclarations in the file, in syntactic order. | ||
const oldImportDecls: ImportDeclaration[] = []; |
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.
You could also filter out invalid imports here, and be able to assume that getExternalModuleName
returns a defined result everywhere else.
src/services/organizeImports.ts
Outdated
return coalescedImports; | ||
|
||
// `undefined` is the min value. | ||
function compareIdentifiers(s1: Identifier | undefined, s2: Identifier | 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.
return compareStringsCaseSensitive(s1 && s1.text, s2 && s2.text);
. Also, these functions don't use any closure variables.
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'm accustomed to syntactic scopes conveying encapsulation. If it is more idiomatic for them to express closure, then I agree that these functions should not be nested.
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.
Opinions on that vary around the team, so whichever is fine.
src/services/organizeImports.ts
Outdated
} | ||
} | ||
|
||
for (const namedImport of namedImports) { |
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.
newImportSpecifiers.push(...flatMap(namedImports, n => n.elements));
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 figured there had to be a function that did that. I was looking for "selectMany".
src/services/organizeImports.ts
Outdated
const groupedImports = groupSortedImports(sortedImports); | ||
for (const importGroup of groupedImports) { | ||
|
||
let seenImportWithoutClause = false; |
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.
Think this could be its own function:
function getImportParts(importGroup: ReadonlyArray<ImportDeclaration>
): {
importWithoutClause: ImportDeclaration | undefined,
defaultImports: ReadonlyArray<Identifier>,
namespaceImports: ReadonlyArray<NamespaceImport>,
namedImports: ReadonlyArray<NamedImports>,
} {
let importWithoutClause: ImportDeclaration;
const defaultImports: Identifier[] = [];
const namespaceImports: NamespaceImport[] = [];
const namedImports: NamedImports[] = [];
for (const importDeclaration of importGroup) {
if (importDeclaration.importClause === undefined) {
// Only the first such import is interesting - the others are redundant.
// Note: Unfortunately, we will lose trivia that was on this node.
importWithoutClause = importDeclaration;
continue;
}
const { name, namedBindings } = importDeclaration.importClause;
if (name) {
defaultImports.push(name);
}
if (namedBindings) {
if (isNamespaceImport(namedBindings)) {
namespaceImports.push(namedBindings);
}
else {
namedImports.push(namedBindings);
}
}
}
return { importWithoutClause, defaultImports, namespaceImports, namedImports };
}
then:
const { importWithoutClause, defaultImports, namedImports, namespaceImports } = getImportParts(importGroup);
if (importWithoutClause) {
coalescedImports.push(importWithoutClause);
}
...proceed as before...
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.
Sure.
src/services/organizeImports.ts
Outdated
} | ||
} | ||
|
||
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => { |
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.
stableSort(newImportSpecifiers, (s1, s2) =>
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) || compareIdentifiers(s1.name, s2.name));
src/services/organizeImports.ts
Outdated
continue; | ||
} | ||
|
||
let newDefaultImport: Identifier = 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.
Identifier | undefined
not Identifier = undefined
src/services/organizeImports.ts
Outdated
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext }); | ||
|
||
// NB: Stopping before i === 0 | ||
for (let i = oldImportDecls.length - 1; i > 0; i--) { |
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.
Could you add a comment explaining why this is in reverse order? I assume this has something to do with trivia?
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.
It's not - I was thinking of something else.
} | ||
|
||
const oldValidImportDecls = oldImportDecls.filter(importDecl => getExternalModuleName(importDecl.moduleSpecifier)); | ||
const oldInvalidImportDecls = oldImportDecls.filter(importDecl => !getExternalModuleName(importDecl.moduleSpecifier)); |
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.
Not sure this is needed -- we're basically just deleting these and adding them back, could just ignore them entirely and make oldImportDecls
only be the valid ones.
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.
If they are scattered throughout the file, this will group them together at the top.
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.
OK. For the case where you do keep the invalid ones, could use a helper function and write const [oldValidImportDecls, oldInvalidImportDecls] = partition(oldImportDecls, isValidImportDeclaration);
src/services/organizeImports.ts
Outdated
}); | ||
} | ||
|
||
function getExternalModuleName(specifier: Expression) { |
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.
Could have a ValidImportDeclaration
type that always has a valid specifier, and then not need to use this function or worry about undefined
results.
interface ValidImportDeclaration extends ImportDeclaration { moduleSpecifier: StringLiteral; }
function isValidImportDeclaration(node: Node): node is ValidImportDeclaration {
return isImportDeclaration(node) && isStringLiteral(node.moduleSpecifier);
}
then:
const oldImportDecls = sourceFile.statements.filter(isValidImportDeclaration);
(We could technically allow NoSubstitutionTemplateLiteral
s here, but they're actually compile errors so not really valid.)
a398e71
to
7498043
Compare
Restarting tests since lint error line numbers didn't match current source. |
I am unable to repro the failure |
@amcasey Might have to do with newlines? |
@andy-ms Conceivably, but it would be strange to have only this one failure. Edit: They're hard-coded to use |
We have many files with mixed line endings, but my current hypothesis is that the problem was introduced when I renamed the baseline output directory from "OrganizeImports" to "organizeImports" for consistency with the others. I'll try to purge the old name locally and then force push. |
Other than this irritating issue, is this more or less ready to merge? |
In phase 1, it coalesces imports from the same module and sorts the results, but does not remove unused imports. Some trivia is lost during coalescing, but none should be duplicated.
@@ -547,6 +551,27 @@ namespace ts.server.protocol { | |||
renameFilename?: 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.
This comment might be better in organizeImports.ts
(the non-test one).
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 can move it when I implement remove-unused.
Eliminate cancellation token Add organizeImports.ts to tsconfig.json Simplify ts.OrganizeImports.organizeImports Simplify sortImports Semantic change: all invalid module specifiers are now considered to be equal. Simplify comparisons using || Pull out imports with invalid modules specifiers ...for separate processing. They are tacked on to the end of the organized imports in their original order. Bonus: downstream functions can now assume imports have valid module specifiers. Rename baseline folder with leading lowercase Simplify coalesceImports Remove some unnecessary null checks Simplify baseline generation
0c2babc
to
5c278ce
Compare
I've revised history so that the baseline folder has always been lowercase. Hopefully, that will fix the tests. |
How do you invoke this command in Visual Studio? I've been completely unable to get it to appear anywhere since upgrading to 2.8. |
This is a new command, so requires update to VS as well. Should light up in your next VS update. |
In phase 1, it coalesces imports from the same module and sorts the
results, but does not remove unused imports.
Some trivia is lost during coalescing, but none should be duplicated.