Skip to content
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

Merged
merged 4 commits into from
Feb 16, 2018
Merged

Conversation

amcasey
Copy link
Member

@amcasey amcasey commented Feb 12, 2018

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.

@amcasey amcasey requested review from mhegazy and a user February 12, 2018 23:09
@amcasey
Copy link
Member Author

amcasey commented Feb 12, 2018

This will need to be reconciled with #21876.

}
}

const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => {
Copy link
Member Author

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.

if (isImportDeclaration(node)) {
oldImportDecls.push(node);
}
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
Copy link
Member Author

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.

@amcasey
Copy link
Member Author

amcasey commented Feb 13, 2018

I don't understand the build failures. I ran jake runtests-parallel locally and it claims to have run the same link command as the server. It reported no errors. However, when I ran the same lint command manually, its output matched the Jenkins console output. Is it possible that runtests-parallel is swallowing (or not waiting for) the lint errors? /cc @weswigham

@amcasey
Copy link
Member Author

amcasey commented Feb 13, 2018

e:\ts_gh>jake runtests-parallel
Discovered 100 unittest suites.
Discovering runner-based tests...
Discovered 11829 test files in 504ms.
Starting to run tests using 8 threads...
Batching initial test lists...
Batched into 8 groups with approximate total time of 1m34s in each group. (90.0% of total tests batched)

  [▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬] √ 59191 passing (2m9s)


  59191 passing (2m)

Linting: node node_modules/tslint/bin/tslint Gulpfile.ts scripts/generateLocalizedDiagnosticMessages.ts "scripts/tslint/**/*.ts" "src/**/*.ts" --exclude "src/lib/*.d.ts" --formatters-dir ./built/local/tslint/formatters --format autolinkableStylish
rm -rf tests\baselines\local\projectOutput\

e:\ts_gh>

@weswigham
Copy link
Member

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>) {
Copy link

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.

Copy link
Member Author

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?

Copy link

@ghost ghost Feb 13, 2018

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.

Copy link
Member Author

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?

Copy link

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> {
Copy link

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", () => {
Copy link

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

Copy link
Member Author

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(
Copy link

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";`)

Copy link
Member Author

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[] = [];
Copy link

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).

Copy link
Member Author

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?

Copy link

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.

Copy link
Member

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#

cancellationToken: CancellationToken) {

// All of the (old) ImportDeclarations in the file, in syntactic order.
const oldImportDecls: ImportDeclaration[] = [];
Copy link

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...

Copy link

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.

Copy link
Member Author

@amcasey amcasey Feb 14, 2018

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.

cancellationToken.throwIfCancellationRequested();

// All of the (new) ImportDeclarations in the file, in sorted order.
const newImportDecls = coalescedImportDecls;
Copy link

@ghost ghost Feb 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just name coalescedImportDecls newImportDecls?

Copy link
Member Author

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.

Copy link

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.

Copy link
Member Author

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.

Copy link

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.

}
else {
// Delete the surrounding trivia because it will have been retained in newImportDecls.
const replaceOptions = {
Copy link

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions);
}

const changes = changeTracker.getChanges();
Copy link

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.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

}

function getExternalModuleName(specifier: Expression) {
return isStringLiteral(specifier) || isNoSubstitutionTemplateLiteral(specifier)
Copy link

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!


/* @internal */ // Internal for testing
export function sortImports(oldImports: ReadonlyArray<ImportDeclaration>) {
if (oldImports.length < 2) {
Copy link

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 inside stableSort
  • 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.

Copy link
Member Author

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.

Copy link

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.

Copy link
Member Author

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 Comparisons 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.

Copy link

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.

return [];
}

const usedImportDecls = removeUnusedImports(oldImportDecls);
Copy link
Member

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) {
Copy link

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.

Copy link
Member Author

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);
Copy link

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;
}

Copy link

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;
}

Copy link
Member Author

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?

Copy link

@ghost ghost Feb 14, 2018

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).

Copy link

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.

Copy link
Member Author

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?

Copy link

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?

Copy link
Member Author

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?

Copy link
Member Author

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.

cancellationToken: CancellationToken) {

// All of the (old) ImportDeclarations in the file, in syntactic order.
const oldImportDecls: ImportDeclaration[] = [];
Copy link

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.

return coalescedImports;

// `undefined` is the min value.
function compareIdentifiers(s1: Identifier | undefined, s2: Identifier | undefined) {
Copy link

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.

Copy link
Member Author

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.

Copy link

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.

}
}

for (const namedImport of namedImports) {
Copy link

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));

Copy link
Member Author

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".

const groupedImports = groupSortedImports(sortedImports);
for (const importGroup of groupedImports) {

let seenImportWithoutClause = false;
Copy link

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...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

}
}

const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => {
Copy link

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));

continue;
}

let newDefaultImport: Identifier = undefined;
Copy link

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

const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext });

// NB: Stopping before i === 0
for (let i = oldImportDecls.length - 1; i > 0; i--) {
Copy link

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?

Copy link
Member Author

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));
Copy link

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.

Copy link
Member Author

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.

Copy link

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);

});
}

function getExternalModuleName(specifier: Expression) {
Copy link

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 NoSubstitutionTemplateLiterals here, but they're actually compile errors so not really valid.)

@amcasey
Copy link
Member Author

amcasey commented Feb 15, 2018

Restarting tests since lint error line numbers didn't match current source.

@amcasey
Copy link
Member Author

amcasey commented Feb 16, 2018

I am unable to repro the failure The baseline file organizeImports/CoalesceMultipleModules.ts has changed.

@ghost
Copy link

ghost commented Feb 16, 2018

@amcasey Might have to do with newlines?

@amcasey
Copy link
Member Author

amcasey commented Feb 16, 2018

@andy-ms Conceivably, but it would be strange to have only this one failure.

Edit: They're hard-coded to use \n.

@amcasey
Copy link
Member Author

amcasey commented Feb 16, 2018

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.

@amcasey
Copy link
Member Author

amcasey commented Feb 16, 2018

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;
}

/**
Copy link

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).

Copy link
Member Author

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
@amcasey
Copy link
Member Author

amcasey commented Feb 16, 2018

I've revised history so that the baseline folder has always been lowercase. Hopefully, that will fix the tests.

@MgSam
Copy link

MgSam commented Apr 5, 2018

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.

@mhegazy
Copy link
Contributor

mhegazy commented Apr 5, 2018

This is a new command, so requires update to VS as well. Should light up in your next VS update.

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants