org.eclipse.jdt.internal.core.dom.rewrite.imports.OrderPreservingImportAdder 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
*******************************************************************************/
package org.eclipse.jdt.internal.core.dom.rewrite.imports;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeSet;
/**
* Keeping existing imports in their existing order, inserts each new import before or after the
* import to which it would be adjacent if all (existing and new) imports were totally ordered
* together.
*
* A new import that would sort between two existing imports which are not adjacent in the
* existing order will be placed adjacent to the existing import with which it shares a longer
* prefix of dot-separated name segments.
*/
final class OrderPreservingImportAdder implements ImportAdder {
static class AdjacentImports {
final Collection importsBefore = new ArrayList<>();
final Collection importsAfter = new ArrayList<>();
@Override
public String toString() {
return String.format("(%s, %s)", this.importsBefore.toString(), this.importsAfter.toString()); //$NON-NLS-1$
}
}
/**
* Returns the number of prefixing dot-separated segments shared between the two names.
*
* For example, {@code countMatchingPrefixSegments("foo.pack1.Class", "foo.pack2.Class")} will
* return 1 and {@code countMatchingPrefixSegments("foo.pack1.Class", "com.foo.pack1.Class")}
* will return 0.
*/
private static int countMatchingPrefixSegments(String name1, String name2) {
if (name1.isEmpty() || name2.isEmpty()) {
return 0;
}
int matchingSegments = 0;
for (int i = 0; i <= name1.length() && i <= name2.length(); i++) {
boolean atEndOfName1Segment = i == name1.length() || name1.charAt(i) == '.';
boolean atEndOfName2Segment = i == name2.length() || name2.charAt(i) == '.';
if (atEndOfName1Segment && atEndOfName2Segment) {
matchingSegments++;
} else if (atEndOfName1Segment || atEndOfName2Segment) {
break;
} else if (name1.charAt(i) != name2.charAt(i)) {
break;
}
}
return matchingSegments;
}
private final Comparator importComparator;
OrderPreservingImportAdder(Comparator importComparator) {
this.importComparator = importComparator;
}
@Override
public List addImports(Collection existingImports, Collection importsToAdd) {
if (importsToAdd.isEmpty()) {
return new ArrayList<>(existingImports);
}
List sortedNewImports = new ArrayList<>(importsToAdd);
sortedNewImports.removeAll(new HashSet<>(existingImports));
Collections.sort(sortedNewImports, this.importComparator);
if (existingImports.isEmpty()) {
return sortedNewImports;
}
Map adjacentNewImports =
determineAdjacentNewImports(new ArrayList<>(existingImports), sortedNewImports);
List importsWithAdditions =
new ArrayList<>(existingImports.size() + sortedNewImports.size());
for (ImportName existingImport : existingImports) {
// Remove the adjacent imports so they don't get inserted multiple times in the case
// of duplicate imports.
AdjacentImports adjacentImports = adjacentNewImports.remove(existingImport);
if (adjacentImports != null) {
importsWithAdditions.addAll(adjacentImports.importsBefore);
}
importsWithAdditions.add(existingImport);
if (adjacentImports != null) {
importsWithAdditions.addAll(adjacentImports.importsAfter);
}
}
return importsWithAdditions;
}
/**
* Determines which new imports to place before and after each existing import.
*
* Returns a Map where each key is an existing import and each corresponding value is an
* AdjacentImports containing those new imports which should be placed before and after that
* existing import. Each new import will be placed either before or after exactly one existing
* import.
*
* @param existingImports
* Existing imports.
* @param sortedNewImports
* Imports to be added. Must be in order as if sorted by this.importComparator.
*/
private Map determineAdjacentNewImports(
Collection existingImports,
Iterable sortedNewImports) {
NavigableSet existingImportsTreeSet = new TreeSet<>(this.importComparator);
existingImportsTreeSet.addAll(existingImports);
Map adjacentNewImports = new HashMap<>();
for (ImportName existingImport : existingImports) {
adjacentNewImports.put(existingImport, new AdjacentImports());
}
for (ImportName newImport : sortedNewImports) {
ImportName precedingExistingImport = existingImportsTreeSet.lower(newImport);
ImportName succeedingExistingImport = existingImportsTreeSet.higher(newImport);
if (shouldGroupWithSucceeding(newImport, precedingExistingImport, succeedingExistingImport)) {
adjacentNewImports.get(succeedingExistingImport).importsBefore.add(newImport);
} else {
adjacentNewImports.get(precedingExistingImport).importsAfter.add(newImport);
}
}
return adjacentNewImports;
}
/**
* Returns true if the new import should be placed before the existing import that would succeed
* it in sorted order, or false if the new import should be placed after the existing import
* that would precede it in sorted order.
*/
private boolean shouldGroupWithSucceeding(
ImportName newImport, ImportName precedingExistingImport, ImportName succeedingExistingImport) {
if (precedingExistingImport == null) {
return true;
} else if (succeedingExistingImport == null) {
return false;
} else {
String containerName = newImport.containerName;
int prefixSharedWithPreceding =
countMatchingPrefixSegments(containerName, precedingExistingImport.containerName);
int prefixSharedWithSucceeding =
countMatchingPrefixSegments(containerName, succeedingExistingImport.containerName);
return prefixSharedWithSucceeding > prefixSharedWithPreceding;
}
}
}