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

Improve ChangeTracker#deleteNodeInList #24221

Merged
2 commits merged into from
May 17, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,15 @@ namespace ts {
return -1;
}

export function findLastIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
for (let i = startIndex === undefined ? array.length - 1 : startIndex; i >= 0; i--) {
if (predicate(array[i], i)) {
return i;
}
}
return -1;
}

/**
* Returns the first truthy result of `callback`, or else fails.
* This is like `forEach`, but never returns undefined.
Expand Down
59 changes: 29 additions & 30 deletions src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ namespace ts.textChanges {
export class ChangeTracker {
private readonly changes: Change[] = [];
private readonly newFiles: { readonly oldFile: SourceFile, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
private readonly deletedNodesInLists: true[] = []; // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
private readonly deletedNodesInLists = new NodeSet(); // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
private readonly classesWithNodesInsertedAtStart = createMap<ClassDeclaration>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>

public static fromContext(context: TextChangesContext): ChangeTracker {
Expand Down Expand Up @@ -262,35 +262,15 @@ namespace ts.textChanges {
this.deleteNode(sourceFile, node);
return this;
}
const id = getNodeId(node);
Debug.assert(!this.deletedNodesInLists[id], "Deleting a node twice");
this.deletedNodesInLists[id] = true;
if (index !== containingList.length - 1) {
const nextToken = getTokenAtPosition(sourceFile, node.end, /*includeJsDocComment*/ false);
if (nextToken && isSeparator(node, nextToken)) {
// find first non-whitespace position in the leading trivia of the node
const startPosition = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
const nextElement = containingList[index + 1];
/// find first non-whitespace position in the leading trivia of the next node
const endPosition = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, nextElement, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
// shift next node so its first non-whitespace position will be moved to the first non-whitespace position of the deleted node
this.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
}
}
else {
const prev = containingList[index - 1];
if (this.deletedNodesInLists[getNodeId(prev)]) {
const pos = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
const end = getAdjustedEndPosition(sourceFile, node, {});
this.deleteRange(sourceFile, { pos, end });
}
else {
const previousToken = getTokenAtPosition(sourceFile, containingList[index - 1].end, /*includeJsDocComment*/ false);
if (previousToken && isSeparator(node, previousToken)) {
this.deleteNodeRange(sourceFile, previousToken, node);
}
}
}

// Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
// That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`.
Debug.assert(!this.deletedNodesInLists.has(node), "Deleting a node twice");
this.deletedNodesInLists.add(node);
this.deleteRange(sourceFile, {
pos: startPositionToDeleteNodeInList(sourceFile, node),
end: index === containingList.length - 1 ? getAdjustedEndPosition(sourceFile, node, {}) : startPositionToDeleteNodeInList(sourceFile, containingList[index + 1]),
});
return this;
}

Expand Down Expand Up @@ -683,6 +663,19 @@ namespace ts.textChanges {
});
}

private finishTrailingCommaAfterDeletingNodesInList() {
this.deletedNodesInLists.forEach(node => {
const sourceFile = node.getSourceFile();
const list = formatting.SmartIndenter.getContainingList(node, sourceFile);
if (node !== last(list)) return;

const lastNonDeletedIndex = findLastIndex(list, n => !this.deletedNodesInLists.has(n), list.length - 2);
if (lastNonDeletedIndex !== -1) {
this.deleteRange(sourceFile, { pos: list[lastNonDeletedIndex].end, end: startPositionToDeleteNodeInList(sourceFile, list[lastNonDeletedIndex + 1]) });
}
});
}

/**
* Note: after calling this, the TextChanges object must be discarded!
* @param validate only for tests
Expand All @@ -691,6 +684,7 @@ namespace ts.textChanges {
*/
public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
this.finishClassesWithNodesInsertedAtStart();
this.finishTrailingCommaAfterDeletingNodesInList();
const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
for (const { oldFile, fileName, statements } of this.newFiles) {
changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter));
Expand All @@ -703,6 +697,11 @@ namespace ts.textChanges {
}
}

// find first non-whitespace position in the leading trivia of the node
function startPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node): number {
return skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
}

function getClassBraceEnds(cls: ClassLikeDeclaration, sourceFile: SourceFile): [number, number] {
return [findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile).end, findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile).end];
}
Expand Down
17 changes: 17 additions & 0 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,23 @@ namespace ts {
}
return propSymbol;
}

export class NodeSet {
private map = createMap<Node>();

add(node: Node): void {
this.map.set(String(getNodeId(node)), node);
}
has(node: Node): boolean {
return this.map.has(String(getNodeId(node)));
}
forEach(cb: (node: Node) => void): void {
this.map.forEach(cb);
}
some(pred: (node: Node) => boolean): boolean {
return forEachEntry(this.map, pred) || false;
}
}
}

// Display-part writer helpers
Expand Down
4 changes: 3 additions & 1 deletion tests/cases/fourslash/codeFixUnusedIdentifier_all_delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
////function f(a, b) {
//// const x = 0;
////}
////function g(a, b, c) { return a; }

verify.codeFixAll({
fixId: "unusedIdentifier_delete",
fixAllDescription: "Delete all unused declarations",
newFileContent:
`function f() {
}`,
}
function g(a) { return a; }`,
});
1 change: 1 addition & 0 deletions tests/cases/fourslash/incompleteFunctionCallCodefix2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

verify.codeFix({
description: "Prefix 'C' with an underscore",
index: 1,
newFileContent: "function f(new _C(100, 3, undefined)",
});