Skip to content

Added alphabetization logic to missing import quick fixes #21876

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

Closed

Conversation

JoshuaKGoldberg
Copy link
Contributor

When generating a codefix to add a missing import to an imports list, instead of always adding to the end, the node will be inserted before the first node its name is < alphabetically (or at the end if < none). Linear search is used instead of binary as the (generally few) nodes are not guaranteed to be in order.

Fixes #16119

Josh Goldberg added 7 commits February 10, 2018 11:34
I think I'll need it for inserting a node before the first in a list.
Searches through the elements until one with a "greater" (using native string compares) one is found, and inserts at that position.

Test cases 8, 10, and 11 are failing.
Copied them as 13, 14, and 15 for inserting in the middle of a list.
Copied them as 16, 17, and 18 for inserting at the end of a list.
* Mismatched test case commas in 8, 12, 13, 16
* Mismatched test case endlines in 11, 15
* Was using `sampleIndex - 1` but passing `0` as that index for the before-first insertion. Switched to just using `sampleIndex`.
* Used last node instead of end of the first node for the ending range when determining line separator style for inserting after the first.
* Inserting before the first in a multiline list needs to insert at position - indentation

Also forgot to `return this;` at the end.
Public first, then private.
@mhegazy
Copy link
Contributor

mhegazy commented Feb 12, 2018

@amcasey and @Andy-MS do you mind reviewing this change?

* Linear search is used instead of binary as the (generally few) nodes are not guaranteed to be in order.
*/
public insertNodeInListAlphabetically(sourceFile: SourceFile, containingList: NodeArray<ImportSpecifier>, newNode: ImportSpecifier): this {
if (newNode.name.text < containingList[0].name.text) {
Copy link

Choose a reason for hiding this comment

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

The way we're using this, containingList will contain at least one element, but it's not obvious if you were just looking at this method.
This may be too specific to belong in textChanges and could just go in importFixes. Though I think insertNodeBefore could be adapted so importFixes doesn't have to call a special insertNodeInListBeforeFirst method or have to call insertNodeInListAfter on a previous element.

}

for (let i = 1; i < containingList.length; i += 1) {
if (newNode.name.text < containingList[i].name.text) {
Copy link

Choose a reason for hiding this comment

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

Not sure, but perhaps compareStringsCaseInsensitive would be appropriate?

@amcasey
Copy link
Member

amcasey commented Feb 12, 2018

What is the sort order of an aliased imported symbol like a as z?

@JoshuaKGoldberg
Copy link
Contributor Author

In this PR, it goes by the .name of the import. So a as z would become z.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 12, 2018

We need to use #21909 for this change.

@JoshuaKGoldberg
Copy link
Contributor Author

I'll wait :)

@Ghabriel
Copy link

#21909 has been merged. What's the status of this pull request?

@JoshuaKGoldberg
Copy link
Contributor Author

JoshuaKGoldberg commented Feb 27, 2018

Thanks for the ping @Ghabriel - I'll take a stab at resolving the merge conflicts hopefully this week.

@JoshuaKGoldberg
Copy link
Contributor Author

@Andy-MS have I addressed your comments?

# Conflicts:
#	src/services/textChanges.ts
@Ghabriel
Copy link

@amcasey @mhegazy @Andy-MS can you merge this?

@mhegazy mhegazy requested a review from amcasey May 30, 2018 22:21
Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

Still a few concerns.

/**
* This function should be used to insert nodes alphabetically in lists when nodes don't carry separators as the part of the node range,
* e.g. export lists, import lists, etc.
* Linear search is used instead of binary as the (generally few) nodes are not guaranteed to be in order.
Copy link
Member

@amcasey amcasey Jun 1, 2018

Choose a reason for hiding this comment

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

I would have thought there was nothing to do if the nodes were not already in order. Would it make sense to do a pre-pass to check whether they're in order then binary search if they are and add to the end if they're not?

*/
function insertNodeInsideListNodesAlphabetically(context: SymbolContext, sourceFile: SourceFile, containingList: NodeArray<ImportSpecifier>, newNode: ImportSpecifier) {
return ChangeTracker.with(context, t => {
if (compareStringsCaseInsensitive(newNode.name.text, containingList[0].name.text) === Comparison.LessThan) {
Copy link
Member

Choose a reason for hiding this comment

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

I can see why either the first iteration or the last iteration has to be special, but not why both need to be pulled out.

@@ -483,107 +483,150 @@ namespace ts.textChanges {
if (index < 0) {
return this;
}
const end = after.getEnd();
if (index !== containingList.length - 1) {
// any element except the last one
// use next sibling as an anchor
const nextToken = getTokenAtPosition(sourceFile, after.end, /*includeJsDocComment*/ false);
if (nextToken && isSeparator(after, nextToken)) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this actually changed, but does the insertion just silently fail if there's no next token or it's not a separator?

}
}

public insertNodeInListBeforeFirst(sourceFile: SourceFile, containingList: NodeArray<Node>, first: Node, newNode: Node): void {
Copy link
Member

Choose a reason for hiding this comment

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

Will first always be containingList[0]?

*/
function insertNodeInsideListNodesAlphabetically(context: SymbolContext, sourceFile: SourceFile, containingList: NodeArray<ImportSpecifier>, newNode: ImportSpecifier) {
return ChangeTracker.with(context, t => {
if (compareStringsCaseInsensitive(newNode.name.text, containingList[0].name.text) === Comparison.LessThan) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd be happier if this were more tightly coupled to organizeImports so that they don't get out of sync. Maybe we could export compareIdentifiers from organizeImports.ts and use it here?

let separator: SyntaxKind.CommaToken | SyntaxKind.SemicolonToken;
let multilineList = false;

// insert element after the last element in the list that has more than one item
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this comment makes sense here. Does it need to be moved or rephrased?

const afterStart = after.getStart(sourceFile);
const afterStartLinePosition = getLineStartPositionForPosition(afterStart, sourceFile);

const { multilineList, separator } = this.getMultilineAndSeparatorOfList(sourceFile, containingList, afterStartLinePosition, index - 1, after);
Copy link
Member

Choose a reason for hiding this comment

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

Why - 1?

const afterMinusOneStartLinePosition = getLineStartPositionForPosition(containingList[sampleIndex].getStart(sourceFile), sourceFile);
multilineList = afterMinusOneStartLinePosition !== afterStartLinePosition;
}
if (hasCommentsBeforeLineBreak(sourceFile.text, sampleNode.end)) {
Copy link
Member

Choose a reason for hiding this comment

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

!multilineList &&?

}

public insertNodeInListBeforeFirst(sourceFile: SourceFile, containingList: NodeArray<Node>, first: Node, newNode: Node): void {
const startPosition = first.getStart(sourceFile);
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this might be after leading trivia on first. Does that matter?

}

const last = containingList[containingList.length - 1];
const indentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, last.end, sourceFile, this.formatContext.options);
Copy link
Member

Choose a reason for hiding this comment

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

Why continue until last.end? Isn't startPosition sufficient?

@RyanCavanaugh
Copy link
Member

@JoshuaKGoldberg can you follow up with Andrew's comments/questions? Thanks!

@JoshuaKGoldberg
Copy link
Contributor Author

Will try to get to this soon, thanks for the ping! I'll close the issue if I don't think I'll have time within a month.

@RyanCavanaugh RyanCavanaugh added this to the Community milestone Sep 17, 2018
@JoshuaKGoldberg
Copy link
Contributor Author

😢

@JoshuaKGoldberg JoshuaKGoldberg deleted the sorted-import-fix branch March 22, 2022 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding an import with quick fix should respect sort order
5 participants