org.eclipse.jdt.internal.core.dom.rewrite.imports.ImportEditor Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2015 Google Inc and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303
* Lars Vogel - Contributions for
* Bug 473178
*******************************************************************************/
package org.eclipse.jdt.internal.core.dom.rewrite.imports;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.text.IRegion;
import org.eclipse.text.edits.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MoveSourceEdit;
import org.eclipse.text.edits.MoveTargetEdit;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.RangeMarker;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
/**
* Creates TextEdits to apply changes to the order of import declarations to a compilation unit.
*/
final class ImportEditor {
/**
* Iterates through the compilation unit's original import order, providing in turn each
* original import and the original start position of that import's leading delimiter.
*/
private static final class OriginalImportsCursor {
private final Iterator originalImportIterator;
OriginalImportEntry currentOriginalImport;
int currentPosition;
OriginalImportsCursor(int startPosition, Collection originalImportEntries) {
this.originalImportIterator = originalImportEntries.iterator();
this.currentPosition = startPosition;
this.currentOriginalImport =
this.originalImportIterator.hasNext() ? this.originalImportIterator.next() : null;
}
/**
* Advances this cursor to the next import in the original order.
*/
void advance() {
IRegion declarationAndComments = this.currentOriginalImport.declarationAndComments;
this.currentPosition = declarationAndComments.getOffset() + declarationAndComments.getLength();
this.currentOriginalImport =
this.originalImportIterator.hasNext() ? this.originalImportIterator.next() : null;
}
}
private static final class ImportEdits {
final Collection leadingDelimiterEdits;
final Collection commentAndDeclarationEdits;
ImportEdits(
Collection leadingDelimiterEdits,
Collection commentAndDeclarationEdits) {
this.leadingDelimiterEdits = leadingDelimiterEdits;
this.commentAndDeclarationEdits = commentAndDeclarationEdits;
}
}
/**
* Maps by identity each import (as key), except the last, to the import (as value) which comes
* before it.
*
* Maps by identity (rather than by hashcode) to handle cases of duplicate import declarations.
*/
private static Map mapPrecedingImports(Collection importEntries) {
Map precedingImports =
new IdentityHashMap<>(importEntries.size());
ImportEntry previousImport = null;
for (ImportEntry currentImport : importEntries) {
ImportName currentImportName = currentImport.importName;
precedingImports.put(currentImportName, previousImport);
previousImport = currentImport;
}
return precedingImports;
}
private static boolean containsFloatingComment(Iterable comments) {
for (ImportComment comment : comments) {
if (comment.succeedingLineDelimiters > 1) {
return true;
}
}
return false;
}
private final String lineDelimiter;
private final String twoLineDelimiters;
private final boolean fixAllLineDelimiters;
private final int lineDelimitersBetweenImportGroups;
private final ImportGroupComparator importGroupComparator;
private final RemovedImportCommentReassigner commentReassigner;
private final Map originalPrecedingImports;
private final List originalImportEntries;
private final RewriteSite rewriteSite;
private final ImportDeclarationWriter declarationWriter;
/**
* @param lineDelimiter
* the string to use as a line delimiter when generating text edits
* @param fixAllLineDelimiters
* specifies whether to standardize whitespace between all imports (if true), or only
* between pairs of imports not originally subsequent (if false)
* @param lineDelimitersBetweenImportGroups
* the number of line delimiters desired between import declarations matching
* different import groups
* @param importGroupComparator
* used to determine whether two subsequent imports match the same import group
* @param originalImports
* the original order of imports in the compilation unit
* @param rewriteSite
* describes the location in the compilation unit where imports shall be rewriten
* @param importDeclarationWriter
* used to render each new import declaration (one not originally present in the
* compilation unit) as a string
*/
ImportEditor(
String lineDelimiter,
boolean fixAllLineDelimiters,
int lineDelimitersBetweenImportGroups,
ImportGroupComparator importGroupComparator,
List originalImports,
RewriteSite rewriteSite,
ImportDeclarationWriter importDeclarationWriter) {
this.lineDelimiter = lineDelimiter;
this.twoLineDelimiters = this.lineDelimiter.concat(this.lineDelimiter);
this.fixAllLineDelimiters = fixAllLineDelimiters;
this.lineDelimitersBetweenImportGroups = lineDelimitersBetweenImportGroups;
this.importGroupComparator = importGroupComparator;
this.originalImportEntries = originalImports;
this.rewriteSite = rewriteSite;
this.declarationWriter = importDeclarationWriter;
this.commentReassigner = new RemovedImportCommentReassigner(originalImports);
if (fixAllLineDelimiters) {
this.originalPrecedingImports = Collections.emptyMap();
} else {
this.originalPrecedingImports = Collections.unmodifiableMap(mapPrecedingImports(originalImports));
}
}
/**
* Generates and returns a TextEdit to replace or update the import declarations in the
* compilation unit to match the given list.
*
* Standardizes whitespace between subsequent imports to the correct number of line delimiters
* (either for every pair of subsequent imports, or only for pairs not originally subsequent,
* depending on the value of {@link #fixAllLineDelimiters}).
*
* Relocates leading and trailing comments of removed imports as determined by
* {@link #commentReassigner}.
*/
TextEdit createTextEdit(Collection resultantImports) {
TextEdit edit = new MultiTextEdit();
IRegion surroundingRegion = this.rewriteSite.surroundingRegion;
if (resultantImports.isEmpty()) {
if (this.originalImportEntries.isEmpty()) {
// Leave the compilation unit as is.
}
else {
// Replace original imports and surrounding whitespace with enough line delimiters
// around preceding and/or succeeding elements.
String newWhitespace;
if (this.rewriteSite.hasPrecedingElements) {
int newDelims = this.rewriteSite.hasSucceedingElements ? 2 : 1;
newWhitespace = createDelimiter(newDelims);
} else {
newWhitespace = ""; //$NON-NLS-1$
}
edit.addChild(new ReplaceEdit(
surroundingRegion.getOffset(), surroundingRegion.getLength(), newWhitespace));
}
}
else {
if (this.originalImportEntries.isEmpty()) {
// Replace existing whitespace with preceding line delimiters, import declarations,
// and succeeding line delimiters.
Collection importEdits = determineEditsForImports(
surroundingRegion, resultantImports);
if (this.rewriteSite.hasPrecedingElements) {
edit.addChild(new InsertEdit(surroundingRegion.getOffset(), createDelimiter(2)));
}
edit.addChildren(importEdits.toArray(new TextEdit[importEdits.size()]));
int newSucceedingDelims = this.rewriteSite.hasSucceedingElements ? 2 : 1;
String newSucceeding = createDelimiter(newSucceedingDelims);
edit.addChild(new InsertEdit(surroundingRegion.getOffset(), newSucceeding));
}
else {
// Replace original imports with new ones, leaving surrounding whitespace in place.
Collection importEdits = determineEditsForImports(
this.rewriteSite.importsRegion, resultantImports);
edit.addChildren(importEdits.toArray(new TextEdit[importEdits.size()]));
}
}
return edit;
}
/**
* Concatenates the given number of line delimiters into a single string.
*/
private String createDelimiter(int numberOfLineDelimiters) {
if (numberOfLineDelimiters < 1) {
throw new IllegalArgumentException();
}
if (numberOfLineDelimiters == 1) {
return this.lineDelimiter;
}
if (numberOfLineDelimiters == 2) {
return this.twoLineDelimiters;
}
StringBuilder correctDelimiter = new StringBuilder();
for (int i = 0; i < numberOfLineDelimiters; i++) {
correctDelimiter.append(this.lineDelimiter);
}
return correctDelimiter.toString();
}
private Collection determineEditsForImports(
IRegion importsRegion,
Collection resultantImports) {
Collection edits = new ArrayList<>();
Map> commentReassignments =
this.commentReassigner.reassignComments(resultantImports);
OriginalImportsCursor cursor = new OriginalImportsCursor(
importsRegion.getOffset(), this.originalImportEntries);
edits.addAll(placeResultantImports(cursor, resultantImports, commentReassignments));
edits.addAll(deleteRemainingText(importsRegion, edits));
// Omit the RangeMarkers used temporarily to mark the text of non-relocated imports.
Collection editsWithoutRangeMarkers = new ArrayList<>(edits.size());
for (TextEdit edit : edits) {
if (!(edit instanceof RangeMarker)) {
editsWithoutRangeMarkers.add(edit);
}
}
return editsWithoutRangeMarkers;
}
/**
* Creates TextEdits that place each resultant import in the correct (rewritten) position.
*/
private Collection placeResultantImports(
OriginalImportsCursor cursor,
Collection resultantImports,
Map> commentReassignments) {
Collection edits = new ArrayList<>();
ImportEntry lastResultantImport = null;
for (ImportEntry currentResultantImport : resultantImports) {
if (currentResultantImport.isOriginal()) {
// Skip forward to this import's place in the original order.
while (cursor.currentOriginalImport != null
&& cursor.currentOriginalImport != currentResultantImport) {
cursor.advance();
}
}
Collection reassignedComments = commentReassignments.get(currentResultantImport);
if (reassignedComments == null) {
reassignedComments = Collections.emptyList();
}
ImportEdits importPlacement;
if (currentResultantImport.isOriginal()) {
OriginalImportEntry originalImport = currentResultantImport.asOriginalImportEntry();
if (cursor.currentOriginalImport == currentResultantImport) {
importPlacement = preserveStationaryImport(originalImport);
} else {
importPlacement = moveOriginalImport(originalImport, cursor.currentPosition);
}
} else {
importPlacement = placeNewImport(currentResultantImport, cursor.currentPosition);
}
String newDelimiter = determineNewDelimiter(
lastResultantImport, currentResultantImport, reassignedComments);
if (newDelimiter == null) {
edits.addAll(importPlacement.leadingDelimiterEdits);
} else if (!newDelimiter.isEmpty()) {
edits.add(new InsertEdit(cursor.currentPosition, newDelimiter));
}
if (!reassignedComments.isEmpty()) {
edits.addAll(relocateComments(reassignedComments, cursor.currentPosition));
boolean hasFloatingComment = currentResultantImport.isOriginal()
&& containsFloatingComment(currentResultantImport.asOriginalImportEntry().comments);
String delimiterAfterReassignedComments =
hasFloatingComment ? this.twoLineDelimiters : this.lineDelimiter;
edits.add(new InsertEdit(cursor.currentPosition, delimiterAfterReassignedComments));
}
edits.addAll(importPlacement.commentAndDeclarationEdits);
if (currentResultantImport == cursor.currentOriginalImport) {
cursor.advance();
}
lastResultantImport = currentResultantImport;
}
return edits;
}
/**
* Creates text edits to insert the text of a new import.
*/
private ImportEdits placeNewImport(ImportEntry currentResultantImport, int position) {
String declaration = this.declarationWriter.writeImportDeclaration(currentResultantImport.importName);
return new ImportEdits(
Collections.emptySet(),
Collections.singleton(new InsertEdit(position, declaration)));
}
/**
* Creates text edits to move an import's text to a new position.
*/
private ImportEdits moveOriginalImport(OriginalImportEntry importEntry, int position) {
MoveSourceEdit leadingSourceEdit = new MoveSourceEdit(
importEntry.leadingDelimiter.getOffset(), importEntry.leadingDelimiter.getLength());
MoveTargetEdit leadingTargetEdit = new MoveTargetEdit(position, leadingSourceEdit);
Collection leadingDelimiterEdits = Arrays.asList(leadingSourceEdit, leadingTargetEdit);
MoveSourceEdit importSourceEdit = new MoveSourceEdit(
importEntry.declarationAndComments.getOffset(), importEntry.declarationAndComments.getLength());
MoveTargetEdit importTargetEdit = new MoveTargetEdit(position, importSourceEdit);
Collection declarationAndCommentEdits = Arrays.asList(importSourceEdit, importTargetEdit);
return new ImportEdits(leadingDelimiterEdits, declarationAndCommentEdits);
}
/**
* Creates RangeMarkers to mark a non-relocated import's text to prevent its deletion.
*/
private ImportEdits preserveStationaryImport(OriginalImportEntry importEntry) {
return new ImportEdits(
Collections.singleton(new RangeMarker(
importEntry.leadingDelimiter.getOffset(),
importEntry.leadingDelimiter.getLength())),
Collections.singleton(new RangeMarker(
importEntry.declarationAndComments.getOffset(),
importEntry.declarationAndComments.getLength())));
}
/**
* Determines whether and how to standardize the whitespace between the end of the previous
* import (or its last trailing comment) and the start of the current import (or its first
* leading comment).
*
* Returns a string containing the correct whitespace to place between the two imports, or
* {@code null} if the current import's original leading whitespace should be preserved.
*/
private String determineNewDelimiter(
ImportEntry lastImport,
ImportEntry currentImport,
Collection reassignedComments) {
if (lastImport == null) {
// The first import in the compilation unit needs no preceding line delimiters.
return ""; //$NON-NLS-1$
}
boolean hasReassignedComments = !reassignedComments.isEmpty();
if (!needsStandardDelimiter(lastImport, currentImport, hasReassignedComments)) {
return null;
}
int numberOfLineDelimiters = 1;
Collection leadingComments;
if (hasReassignedComments) {
leadingComments = reassignedComments;
} else if (currentImport.isOriginal()) {
leadingComments = currentImport.asOriginalImportEntry().comments;
} else {
leadingComments = Collections.emptyList();
}
if (containsFloatingComment(leadingComments)) {
// Prevent a floating leading comment from becoming attached to the preceding import.
numberOfLineDelimiters = 2;
}
if (this.importGroupComparator.compare(lastImport.importName, currentImport.importName) != 0) {
// Separate imports belonging to different import groups.
numberOfLineDelimiters = Math.max(numberOfLineDelimiters, this.lineDelimitersBetweenImportGroups);
}
String standardDelimiter = createDelimiter(numberOfLineDelimiters);
// Reuse the original preceding delimiter if it matches the standard delimiter, but only
// if there are no reassigned comments (which would necessitate relocating the delimiter).
if (currentImport.isOriginal() && !hasReassignedComments) {
OriginalImportEntry originalImport = currentImport.asOriginalImportEntry();
IRegion originalDelimiter = originalImport.leadingDelimiter;
if (originalImport.precedingLineDelimiters == numberOfLineDelimiters) {
boolean delimiterIsSameLength = originalDelimiter == null && standardDelimiter.isEmpty()
|| originalDelimiter != null && originalDelimiter.getLength() == standardDelimiter.length();
if (delimiterIsSameLength) {
return null;
}
}
}
return standardDelimiter;
}
/**
* Determines whether the whitespace between two subsequent imports should be set to a standard
* number of line delimiters.
*/
private boolean needsStandardDelimiter(
ImportEntry lastImport,
ImportEntry currentImport,
boolean hasReassignedComments) {
boolean needsStandardDelimiter = false;
if (this.fixAllLineDelimiters) {
// In "Organize Imports" mode, all delimiters between imports are standardized.
needsStandardDelimiter = true;
} else if (!currentImport.isOriginal()) {
// This (new) import does not have an original leading delimiter.
needsStandardDelimiter = true;
} else if (hasReassignedComments) {
// Comments reassigned from removed imports are being prepended to this import.
needsStandardDelimiter = true;
} else {
ImportEntry originalPrecedingImport = this.originalPrecedingImports.get(currentImport.importName);
if (originalPrecedingImport == null || lastImport.importName != originalPrecedingImport.importName) {
// This import follows a different import post-rewrite than pre-rewrite.
needsStandardDelimiter = true;
}
}
return needsStandardDelimiter;
}
private Collection relocateComments(Collection reassignedComments, int insertPosition) {
if (reassignedComments.isEmpty()) {
return Collections.emptyList();
}
Collection edits = new ArrayList<>(reassignedComments.size() * 3);
ImportComment lastComment = null;
for (ImportComment currentComment : reassignedComments) {
MoveSourceEdit sourceEdit = new MoveSourceEdit(
currentComment.region.getOffset(), currentComment.region.getLength());
edits.add(sourceEdit);
if (lastComment != null) {
// Preserve blank lines between comments.
int succeedingLineDelimiters = lastComment.succeedingLineDelimiters > 1 ? 2 : 1;
edits.add(new InsertEdit(insertPosition, createDelimiter(succeedingLineDelimiters)));
}
edits.add(new MoveTargetEdit(insertPosition, sourceEdit));
lastComment = currentComment;
}
return edits;
}
/**
* Creates TextEdits that delete text remaining between and after resultant imports.
*/
private static Collection deleteRemainingText(IRegion importRegion, Collection edits) {
List sortedEdits = new ArrayList<>(edits);
Collections.sort(sortedEdits, new Comparator() {
@Override
public int compare(TextEdit o1, TextEdit o2) {
return o1.getOffset() - o2.getOffset();
}
});
int deletePosition = importRegion.getOffset();
Collection deleteRemainingTextEdits = new ArrayList<>();
for (TextEdit edit : sortedEdits) {
if (edit.getOffset() > deletePosition) {
deleteRemainingTextEdits.add(new DeleteEdit(deletePosition, edit.getOffset() - deletePosition));
}
int editEndPosition = edit.getOffset() + edit.getLength();
deletePosition = Math.max(deletePosition, editEndPosition);
}
// Delete text remaining after the last import.
int importRegionEndPosition = importRegion.getOffset() + importRegion.getLength();
if (deletePosition < importRegionEndPosition) {
deleteRemainingTextEdits.add(new DeleteEdit(deletePosition, importRegionEndPosition - deletePosition));
}
return deleteRemainingTextEdits;
}
}