blob: 276f5ff975422cb0b3fe2ffe0794178f2b71a574 [file] [log] [blame]
/*******************************************************************************
* 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 <jogl@google.com> - import group sorting is broken - https://bugs.eclipse.org/430303
*******************************************************************************/
package org.aspectj.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.
* <p>
* 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<ImportName> importsBefore = new ArrayList<ImportName>();
final Collection<ImportName> importsAfter = new ArrayList<ImportName>();
@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.
* <p>
* 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<ImportName> importComparator;
OrderPreservingImportAdder(Comparator<ImportName> importComparator) {
this.importComparator = importComparator;
}
@Override
public List<ImportName> addImports(Collection<ImportName> existingImports, Collection<ImportName> importsToAdd) {
if (importsToAdd.isEmpty()) {
return new ArrayList<ImportName>(existingImports);
}
List<ImportName> sortedNewImports = new ArrayList<ImportName>(importsToAdd);
sortedNewImports.removeAll(new HashSet<ImportName>(existingImports));
Collections.sort(sortedNewImports, this.importComparator);
if (existingImports.isEmpty()) {
return sortedNewImports;
}
Map<ImportName, AdjacentImports> adjacentNewImports =
determineAdjacentNewImports(new ArrayList<ImportName>(existingImports), sortedNewImports);
List<ImportName> importsWithAdditions =
new ArrayList<ImportName>(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.
* <p>
* 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<ImportName, AdjacentImports> determineAdjacentNewImports(
Collection<ImportName> existingImports,
Iterable<ImportName> sortedNewImports) {
NavigableSet<ImportName> existingImportsTreeSet = new TreeSet<ImportName>(this.importComparator);
existingImportsTreeSet.addAll(existingImports);
Map<ImportName, AdjacentImports> adjacentNewImports = new HashMap<ImportName, AdjacentImports>();
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;
}
}
}