main.org.openrewrite.kotlin.style.ImportLayoutStyle Maven / Gradle / Ivy
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.kotlin.style;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.jspecify.annotations.Nullable;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.java.tree.*;
import org.openrewrite.kotlin.KotlinStyle;
import org.openrewrite.kotlin.internal.KotlinPrinter;
import org.openrewrite.marker.Markers;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.openrewrite.internal.StreamUtils.distinctBy;
/**
* A Java Style to define how imports are grouped and ordered. Additionally, this style provides configuration to dictate
* how wildcard folding should be applied when multiple imports are in the same package or on the same, statically-imported
* type.
*
* The import layout consist of three properties:
*
*
topLevelSymbolsToUseStarImport - How many imports from the same package must be present before they should be collapsed into a star import. The default is 5.
* javaStaticsAndEnumsToUseStarImport - How many java static and enum imports from the same type must be present before they should be collapsed into a star import. The default is 3.
* layout - An ordered list of import groupings which define exactly how imports should be organized within a compilation unit.
* packagesToFold - An ordered list of packages which are folded when 1 or more types are in use.
*/
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Getter
@JsonDeserialize(using = Deserializer.class)
@JsonSerialize(using = Serializer.class)
public class ImportLayoutStyle implements KotlinStyle {
@EqualsAndHashCode.Include
private final int topLevelSymbolsToUseStarImport;
@EqualsAndHashCode.Include
private final int javaStaticsAndEnumsToUseStarImport;
@EqualsAndHashCode.Include
private final List layout;
@EqualsAndHashCode.Include
private final List packagesToFold;
private final List blocksNoCatchalls;
private final List blocksOnlyCatchalls;
private final boolean importAliasesSeparately;
public ImportLayoutStyle(int topLevelSymbolsToUseStarImport,
int javaStaticsAndEnumsToUseStarImport,
List layout,
List packagesToFold,
boolean importAliasesSeparately) {
this.topLevelSymbolsToUseStarImport = topLevelSymbolsToUseStarImport;
this.javaStaticsAndEnumsToUseStarImport = javaStaticsAndEnumsToUseStarImport;
this.layout = layout.isEmpty() ? IntelliJ.importLayout().getLayout() : layout;
this.packagesToFold = packagesToFold;
// Divide the blocks into those that accept imports from any package ("catchalls") and those that accept imports from only specific packages
Map> blockGroups = layout.stream()
.collect(Collectors.partitioningBy(Block.AllOthers.class::isInstance));
blocksNoCatchalls = blockGroups.get(false);
blocksOnlyCatchalls = blockGroups.get(true);
// This setting is by default enabled on Intellij's UI, but in reality, it's turned off.
this.importAliasesSeparately = importAliasesSeparately;
}
/**
* Adds a new import in a block that best represents the import layout style without
* re-ordering any of the existing imports, i.e. a minimally invasive add.
*
* @param originalImports The import list before inserting.
* @param toAdd The import to add.
* @param pkg A package declaration, if one exists.
* @return The import list with a new import added.
*/
public List> addImport(List> originalImports,
J.Import toAdd, J.@Nullable Package pkg,
Collection classpath) {
JRightPadded paddedToAdd = new JRightPadded<>(toAdd, Space.EMPTY, Markers.EMPTY);
if (originalImports.isEmpty()) {
paddedToAdd = pkg == null ? paddedToAdd : paddedToAdd.withElement(paddedToAdd.getElement().withPrefix(Space.format("\n\n")));
paddedToAdd = isPackageAlwaysFolded(packagesToFold, paddedToAdd.getElement()) ? paddedToAdd.withElement(paddedToAdd.getElement().withQualid(
paddedToAdd.getElement().getQualid().withName(
paddedToAdd.getElement().getQualid().getName().withSimpleName("*")
)
)) : paddedToAdd;
return singletonList(paddedToAdd);
}
// don't star fold just yet, because we are only going to star fold adjacent imports along with
// the import to add at most. we don't even want to star fold other non-adjacent imports in the same
// block that should be star folded according to the layout style (minimally invasive change).
List> ideallyOrdered =
new ImportLayoutStyle(Integer.MAX_VALUE, Integer.MAX_VALUE, layout, packagesToFold, importAliasesSeparately)
.orderImports(ListUtils.concat(originalImports, paddedToAdd), new HashSet<>());
if (ideallyOrdered.size() == originalImports.size()) {
Set originalPaths = new HashSet<>();
for (JRightPadded originalImport : originalImports) {
originalPaths.add(originalImport.getElement().getTypeName());
}
int sharedImports = 0;
for (JRightPadded importJRightPadded : ideallyOrdered) {
if (originalPaths.contains(importJRightPadded.getElement().getTypeName())) {
sharedImports++;
}
}
if (sharedImports == originalImports.size()) {
// must be a duplicate of an existing import
return originalImports;
}
}
JRightPadded before = null;
JRightPadded after = null;
Block addToBlock = block(paddedToAdd);
int insertPosition = 0;
for (int i = 0; i < ideallyOrdered.size(); i++) {
JRightPadded anImport = ideallyOrdered.get(i);
if (anImport.getElement().isScope(paddedToAdd.getElement())) {
before = i > 0 ? ideallyOrdered.get(i - 1) : null;
after = i + 1 < ideallyOrdered.size() ? ideallyOrdered.get(i + 1) : null;
if (before != null) {
// Use the "before" import to determine insertion point.
// Find the import in the original list to establish insertion position.
for (int j = 0; j < originalImports.size(); j++) {
if (after != null && after.getElement().equals(originalImports.get(j).getElement()) && addToBlock.accept(after)) {
break;
} else if (before.getElement().equals(originalImports.get(j).getElement())) {
insertPosition = j + 1;
after = insertPosition < originalImports.size() ? originalImports.get(insertPosition) : null;
break;
}
}
} else if (after != null) {
// Otherwise, "after" as the basis for the insertion.
// Find the import in the original list to establish insertion position.
for (int j = 0; j < originalImports.size(); j++) {
if (after.getElement().equals(originalImports.get(j).getElement())) {
insertPosition = j;
before = j > 0 ? originalImports.get(insertPosition - 1) : null;
break;
}
}
}
break;
}
}
AtomicBoolean isNewBlock = new AtomicBoolean(false);
if (!(insertPosition == 0 && pkg == null)) {
if (before == null) {
if (pkg != null) {
Space prefix = originalImports.get(0).getElement().getPrefix();
paddedToAdd = paddedToAdd.withElement(paddedToAdd.getElement().withPrefix(prefix));
}
} else if (block(before) != addToBlock) {
boolean isFound = false;
for (int j = insertPosition; j < originalImports.size(); j++) {
if (block(originalImports.get(j)) == addToBlock) {
insertPosition = j;
after = originalImports.get(j);
isFound = true;
break;
}
}
isNewBlock.set(!isFound);
paddedToAdd = paddedToAdd.withElement(paddedToAdd.getElement().withPrefix(Space.format("\n\n")));
} else {
paddedToAdd = paddedToAdd.withElement(paddedToAdd.getElement().withPrefix(Space.format("\n")));
}
}
List> checkConflicts = new ArrayList<>(originalImports);
checkConflicts.add(paddedToAdd);
boolean isFoldable = new ImportLayoutConflictDetection(classpath, checkConflicts)
.isPackageFoldable(packageOrOuterClassName(paddedToAdd));
// Walk both directions from the insertion point, looking for imports that are in the same block and have the
// same package/outerclassname.
AtomicInteger starFoldFrom = new AtomicInteger(insertPosition);
AtomicInteger starFoldTo = new AtomicInteger(insertPosition);
AtomicBoolean starFold = new AtomicBoolean(false);
int sameCount = 1; // start at 1 to account for the import being added.
for (int i = insertPosition; i < originalImports.size(); i++) {
JRightPadded anImport = originalImports.get(i);
if (block(anImport) == addToBlock && packageOrOuterClassName(anImport)
.equals(packageOrOuterClassName(paddedToAdd))) {
starFoldTo.set(i + 1);
sameCount++;
} else {
break;
}
}
for (int i = insertPosition - 1; i >= 0; i--) {
JRightPadded anImport = originalImports.get(i);
if (block(anImport) == addToBlock && packageOrOuterClassName(anImport)
.equals(packageOrOuterClassName(paddedToAdd))) {
starFoldFrom.set(i);
sameCount++;
} else {
break;
}
}
if (isFoldable && (((paddedToAdd.getElement().isStatic() && javaStaticsAndEnumsToUseStarImport <= sameCount) ||
(!paddedToAdd.getElement().isStatic() && topLevelSymbolsToUseStarImport <= sameCount)) || isPackageAlwaysFolded(packagesToFold, paddedToAdd.getElement()))) {
starFold.set(true);
if (insertPosition != starFoldFrom.get()) {
// if we're adding to the middle of a group of imports that are getting star folded,
// adopt the prefix of the first import in this group.
paddedToAdd = paddedToAdd.withElement(paddedToAdd.getElement().withPrefix(
originalImports.get(starFoldFrom.get()).getElement().getPrefix()
));
}
}
if (starFold.get()) {
paddedToAdd = paddedToAdd.withElement(paddedToAdd.getElement().withQualid(
paddedToAdd.getElement().getQualid().withName(
paddedToAdd.getElement().getQualid().getName().withSimpleName("*")
)
));
after = starFoldTo.get() < originalImports.size() ?
originalImports.get(starFoldTo.get()) : null;
}
if (after != null) {
if (block(after) == addToBlock) {
after = after.withElement(after.getElement().withPrefix(Space.format("\n")));
} else if (!isNewBlock.get() && after.getElement().getPrefix().getLastWhitespace().chars()
.filter(c -> c == '\n').count() < 2) {
after = after.withElement(after.getElement().withPrefix(Space.format("\n\n")));
}
}
JRightPadded finalToAdd = paddedToAdd;
JRightPadded finalAfter = after;
return ListUtils.flatMap(originalImports, (i, anImport) -> {
if (starFold.get() && i >= starFoldFrom.get() && i < starFoldTo.get()) {
return i == starFoldFrom.get() ?
finalToAdd /* only add the star import once */ :
null;
} else if (finalAfter != null && anImport.getElement().isScope(finalAfter.getElement())) {
if (starFold.get()) {
// The added import is always folded, and is the first package occurence in the imports.
if (starFoldFrom.get() == starFoldTo.get()) {
return Arrays.asList(finalToAdd, finalAfter);
} else {
return finalAfter;
}
} else if (isNewBlock.get()) {
return anImport.getElement().isStatic() && !finalToAdd.getElement().isStatic() ?
Arrays.asList(finalToAdd, finalAfter) : Arrays.asList(finalAfter, finalToAdd);
} else {
return Arrays.asList(finalToAdd, finalAfter);
}
} else if (i == originalImports.size() - 1 && (finalAfter == null)) {
return Arrays.asList(anImport, finalToAdd);
}
return anImport;
});
}
private Block block(JRightPadded anImport) {
for (Block block : layout) {
if (block.accept(anImport)) {
return block;
}
}
throw new IllegalStateException("Expected to find a block to fit import into.");
}
/**
* This method will order and group a list of imports producing a new list that conforms to the rules defined
* by the import layout style.
*
* @param originalImports A list of potentially unordered imports.
* @return A list of imports that are grouped and ordered.
*/
public List> orderImports(List> originalImports, Collection classpath) {
LayoutState layoutState = new LayoutState();
ImportLayoutConflictDetection importLayoutConflictDetection = new ImportLayoutConflictDetection(classpath, originalImports);
List> orderedImports = new ArrayList<>();
// Allocate imports to blocks, preferring to put imports into non-catchall blocks
nextImport:
for (JRightPadded anImport : originalImports) {
for (Block block : blocksNoCatchalls) {
if (block.accept(anImport)) {
layoutState.claimImport(block, anImport);
continue nextImport;
}
}
for (Block block : blocksOnlyCatchalls) {
if (block.accept(anImport)) {
layoutState.claimImport(block, anImport);
continue nextImport;
}
}
}
int importIndex = 0;
int extraLineSpaceCount = 0;
String prevWhitespace = "";
for (Block block : layout) {
if (block instanceof Block.BlankLines) {
extraLineSpaceCount = 0;
for (int i = 0; i < ((Block.BlankLines) block).getCount(); i++) {
extraLineSpaceCount += 1;
}
} else {
List> blockOrdering = block.orderedImports(layoutState, topLevelSymbolsToUseStarImport, javaStaticsAndEnumsToUseStarImport, importLayoutConflictDetection, packagesToFold);
for (JRightPadded orderedImport : blockOrdering) {
boolean whitespaceContainsCRLF = orderedImport.getElement().getPrefix().getWhitespace().contains("\r\n");
Space prefix;
if (importIndex == 0) {
prefix = originalImports.get(0).getElement().getPrefix();
} else {
// Preserve the existing newline character type of either CRLF or LF.
// Classic Mac OS new line return '\r' is replaced by '\n'.
String newLineCharacters = whitespaceContainsCRLF ||
StringUtils.isNullOrEmpty(orderedImport.getElement().getPrefix().getWhitespace()) &&
"\r\n".equals(prevWhitespace) ? "\r\n" : "\n";
StringBuilder newWhitespace = new StringBuilder(newLineCharacters);
for (int i = 0; i < extraLineSpaceCount; i++) {
newWhitespace.append(newLineCharacters);
}
prefix = orderedImport.getElement().getPrefix().withWhitespace(newWhitespace.toString());
}
if (!orderedImport.getElement().getPrefix().equals(prefix)) {
orderedImports.add(orderedImport.withElement(orderedImport.getElement()
.withPrefix(prefix)));
} else {
orderedImports.add(orderedImport);
}
// Imports with null or empty whitespace will be set to the previous prefix.
prevWhitespace = whitespaceContainsCRLF ? "\r\n" : "\n";
extraLineSpaceCount = 0;
importIndex++;
}
}
}
return orderedImports;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final List blocks = new ArrayList<>();
private final List packagesToFold = new ArrayList<>();
private int topLevelSymbolsToUseStarImport = 5;
private int javaStaticsAndEnumsToUseStarImport = 3;
private boolean importAliasesSeparately = false;
public Builder importAllOthers() {
blocks.add(new Block.AllOthers(!importAliasesSeparately));
return this;
}
public Builder importAllAliases() {
blocks.add(new Block.AllAliases());
return this;
}
public Builder blankLine() {
if (!blocks.isEmpty() &&
blocks.get(blocks.size() - 1) instanceof Block.BlankLines) {
((Block.BlankLines) blocks.get(blocks.size() - 1)).count++;
} else {
blocks.add(new Block.BlankLines());
}
return this;
}
public Builder importPackage(String packageWildcard, Boolean withSubpackages) {
blocks.add(new Block.ImportPackage(packageWildcard, withSubpackages, !importAliasesSeparately));
return this;
}
public Builder importPackage(String packageWildcard) {
return importPackage(packageWildcard, true);
}
public Builder packageToFold(String packageWildcard, Boolean withSubpackages) {
packagesToFold.add(new Block.ImportPackage(packageWildcard, withSubpackages, !importAliasesSeparately));
return this;
}
public Builder packageToFold(String packageWildcard) {
return packageToFold(packageWildcard, true);
}
public Builder topLevelSymbolsToUseStarImport(int topLevelSymbolsToUseStarImport) {
this.topLevelSymbolsToUseStarImport = topLevelSymbolsToUseStarImport;
return this;
}
public Builder javaStaticsAndEnumsToUseStarImport(int javaStaticsAndEnumsToUseStarImport) {
this.javaStaticsAndEnumsToUseStarImport = javaStaticsAndEnumsToUseStarImport;
return this;
}
public Builder importAliasesSeparately(boolean importAliasesSeparately) {
this.importAliasesSeparately = importAliasesSeparately;
return this;
}
public ImportLayoutStyle build() {
for (Block block : blocks) {
if (block instanceof Block.AllOthers) {
((Block.AllOthers) block).setPackageImports(blocks.stream()
.filter(b -> b.getClass().equals(Block.ImportPackage.class))
.map(Block.ImportPackage.class::cast)
.collect(toList()));
}
}
return new ImportLayoutStyle(topLevelSymbolsToUseStarImport, javaStaticsAndEnumsToUseStarImport, blocks, packagesToFold, importAliasesSeparately);
}
}
/**
* The in-progress state of a single layout operation.
*/
private static class LayoutState {
Map>> imports = new HashMap<>();
public void claimImport(Block block, JRightPadded import_) {
imports.computeIfAbsent(block, b -> new ArrayList<>()).add(import_);
}
public List> getImports(Block block) {
return imports.getOrDefault(block, emptyList());
}
}
public static boolean isPackageAlwaysFolded(List packagesToFold, J.Import checkImport) {
boolean isPackageFolded = false;
String anImportName = checkImport.getQualid().printTrimmed(new KotlinPrinter<>());
for (Block block : packagesToFold) {
Block.ImportPackage importPackage = (Block.ImportPackage) block;
if (importPackage.packageWildcard.matcher(anImportName).matches()) {
isPackageFolded = true;
}
}
return isPackageFolded;
}
private static class ImportLayoutConflictDetection {
private final Collection classpath;
private final List> originalImports;
private final Set jvmClasspathNames = new HashSet<>();
private @Nullable Set containsClassNameConflict = null;
ImportLayoutConflictDetection(Collection classpath, List> originalImports) {
this.classpath = classpath;
this.originalImports = originalImports;
}
/**
* Checks if folding the package will create a namespace conflict with any other classes that have already been imported.
*
* @param packageName package that qualifies for folding into a '*'.
* @return folding the package will not create any namespace conflicts.
*/
public boolean isPackageFoldable(String packageName) {
if (containsClassNameConflict == null) {
containsClassNameConflict = new HashSet<>();
setJVMClassNames();
Map> nameToPackages = mapNamesInPackageToPackages();
for (String className : nameToPackages.keySet()) {
if (nameToPackages.get(className).size() > 1 || jvmClasspathNames.contains(className)) {
containsClassNameConflict.addAll(nameToPackages.get(className));
}
}
}
return classpath.isEmpty() || !containsClassNameConflict.contains(packageName);
}
private void setJVMClassNames() {
for (JavaType.FullyQualified fqn : classpath) {
if ("java.lang".equals(fqn.getPackageName())) {
jvmClasspathNames.add(fqn.getClassName());
}
}
}
private Map> mapNamesInPackageToPackages() {
Map> nameToPackages = new HashMap<>();
Set checkPackageForClasses = new HashSet<>();
for (JRightPadded anImport : originalImports) {
checkPackageForClasses.add(packageOrOuterClassName(anImport));
nameToPackages.computeIfAbsent(anImport.getElement().getClassName(), p -> new HashSet<>())
.add(anImport.getElement().getPackageName());
}
for (JavaType.FullyQualified classGraphFqn : classpath) {
String packageName = classGraphFqn.getPackageName();
if (checkPackageForClasses.contains(packageName)) {
String className = classGraphFqn.getClassName();
Set packages = nameToPackages.getOrDefault(className, new HashSet<>());
packages.add(packageName);
nameToPackages.put(className, packages);
} else if (checkPackageForClasses.contains(classGraphFqn.getFullyQualifiedName())) {
packageName = classGraphFqn.getFullyQualifiedName();
for (JavaType.Variable member : classGraphFqn.getMembers()) {
if (member.getFlags().contains(Flag.Static)) {
Set packages = nameToPackages.getOrDefault(member.getName(), new HashSet<>());
packages.add(packageName);
nameToPackages.put(member.getName(), packages);
}
}
for (JavaType.Method method : classGraphFqn.getMethods()) {
if (method.getFlags().contains(Flag.Static)) {
Set packages = nameToPackages.getOrDefault(method.getName(), new HashSet<>());
packages.add(packageName);
nameToPackages.put(method.getName(), packages);
}
}
}
}
return nameToPackages;
}
}
/**
* A block represents a grouping of imports based on matching rules. The block provides a mechanism for matching
* and storing J.Imports that belong to the block.
*/
public interface Block {
/**
* This method will determine if the passed in import is a match for the rules defined on the block. If the
* import is matched, it will be internally stored in the block.
*
* @param anImport The import to be compared against the block's matching rules.
* @return {@code true} if the import was a match
*/
boolean accept(JRightPadded anImport);
/**
* @return Imports belonging to this block, folded appropriately.
*/
List> orderedImports(LayoutState layoutState, int classCountToUseStarImport, int nameCountToUseStarImport, ImportLayoutConflictDetection importLayoutConflictDetection, List packagesToFold);
/**
* A specialized block implementation to act as a blank line separator between import groupings.
*/
class BlankLines implements Block {
private int count = 1;
private int getCount() {
return count;
}
@Override
public boolean accept(JRightPadded anImport) {
return false;
}
@Override
public List> orderedImports(LayoutState layoutState, int classCountToUseStarImport, int nameCountToUseStartImport, ImportLayoutConflictDetection importLayoutConflictDetection, List packagesToFold) {
return emptyList();
}
@Override
public String toString() {
return "" + (count > 1 ? " (x" + count + ")" : "");
}
}
@SuppressWarnings({"deprecation", "ConstantValue"})
class ImportPackage implements Block {
boolean acceptAliasImport;
// VisibleForTesting
static final Comparator> IMPORT_SORTING = (i1, i2) -> {
String[] import1 = i1.getElement().getQualid().printTrimmed().split("\\.");
String[] import2 = i2.getElement().getQualid().printTrimmed().split("\\.");
for (int i = 0; i < Math.min(import1.length, import2.length); i++) {
int diff = import1[i].compareTo(import2[i]);
if (diff != 0) {
return diff;
}
}
if (import1.length == import2.length) {
return 0;
}
return import1.length > import2.length ? 1 : -1;
};
private final Pattern packageWildcard;
public ImportPackage(String packageWildcard, boolean withSubpackages, boolean acceptAliasImport) {
this.acceptAliasImport = acceptAliasImport;
this.packageWildcard = Pattern.compile(packageWildcard
.replace(".", "\\.")
.replace("*", withSubpackages ? ".+" : "[^.]+"));
}
public Pattern getPackageWildcard() {
return packageWildcard;
}
@Override
public boolean accept(JRightPadded anImport) {
if ((!acceptAliasImport) && (anImport.getElement().getAlias() != null)) {
return false;
}
return packageWildcard.matcher(anImport.getElement().getQualid().printTrimmed()).matches();
}
@Override
public List> orderedImports(LayoutState layoutState, int classCountToUseStarImport, int nameCountToUseStarImport, ImportLayoutConflictDetection importLayoutConflictDetection, List packagesToFold) {
List> imports = layoutState.getImports(this);
Map>> groupedImports = imports
.stream()
.sorted(IMPORT_SORTING)
.collect(groupingBy(
ImportLayoutStyle::packageOrOuterClassName,
LinkedHashMap::new, // Use an ordered map to preserve sorting
Collectors.toList()
));
List> ordered = new ArrayList<>(imports.size());
for (List> importGroup : groupedImports.values()) {
JRightPadded toStar = importGroup.get(0);
int threshold = toStar.getElement().isStatic() ? nameCountToUseStarImport : classCountToUseStarImport;
boolean starImportExists = importGroup.stream()
.anyMatch(it -> it.getElement().getQualid().getSimpleName().equals("*"));
// Disable folding imports in Kotlin due to https://github.com/openrewrite/rewrite-kotlin/issues/370
boolean disableFoldingImports = true;
if (!disableFoldingImports && importLayoutConflictDetection.isPackageFoldable(packageOrOuterClassName(toStar)) &&
(isPackageAlwaysFolded(packagesToFold, toStar.getElement()) || importGroup.size() >= threshold || (starImportExists && importGroup.size() > 1))) {
J.FieldAccess qualid = toStar.getElement().getQualid();
J.Identifier name = qualid.getName();
Set typeNamesInThisGroup = importGroup.stream()
.map(im -> im.getElement().getClassName())
.collect(Collectors.toSet());
Optional oneOfTheTypesIsInAnotherGroupToo = groupedImports.values().stream()
.filter(group -> group != importGroup)
.flatMap(group -> group.stream()
.filter(im -> typeNamesInThisGroup.contains(im.getElement().getClassName())))
.map(im -> im.getElement().getTypeName())
.findAny();
if (starImportExists || !oneOfTheTypesIsInAnotherGroupToo.isPresent()) {
ordered.add(toStar.withElement(toStar.getElement().withQualid(qualid.withName(name.withSimpleName("*")))));
continue;
}
}
Predicate> predicate = distinctBy(t -> t.getElement().printTrimmed(new KotlinPrinter<>()));
for (JRightPadded importJRightPadded : importGroup) {
if (predicate.test(importJRightPadded)) {
ordered.add(importJRightPadded);
}
}
}
// interleaves inner classes and outer classes back together which are separated into different groups
// above for the sake of determining whether groups of outer class or inner class imports need to be star
// folded/unfolded
ordered.sort(IMPORT_SORTING);
return ordered;
}
@Override
public String toString() {
return "import " + packageWildcard;
}
}
class AllOthers extends ImportPackage {
private Collection packageImports = emptyList();
public AllOthers(boolean acceptAlias) {
super("*", true, acceptAlias);
}
public void setPackageImports(Collection packageImports) {
this.packageImports = packageImports;
}
@Override
public boolean accept(JRightPadded anImport) {
for (ImportPackage pi : packageImports) {
if (pi.accept(anImport)) {
return false;
}
}
return true;
}
@Override
public String toString() {
return "import all other imports";
}
}
class AllAliases extends ImportPackage {
private Collection packageImports = emptyList();
public AllAliases() {
super("*", true, true);
}
public void setPackageImports(Collection packageImports) {
this.packageImports = packageImports;
}
@Override
public boolean accept(JRightPadded anImport) {
for (ImportPackage pi : packageImports) {
if (pi.accept(anImport)) {
return false;
}
}
return anImport.getElement().getAlias() != null;
}
@Override
public String toString() {
return "import all alias imports";
}
}
}
@Override
public String toString() {
StringBuilder s = new StringBuilder();
s.append("topLevelSymbols=")
.append(topLevelSymbolsToUseStarImport)
.append(", javaStaticAndEnums=")
.append(javaStaticsAndEnumsToUseStarImport)
.append('\n');
for (Block block : layout) {
s.append(block).append("\n");
}
return s.toString();
}
private static String packageOrOuterClassName(JRightPadded anImport) {
String typeName = anImport.getElement().getTypeName();
if (anImport.getElement().isStatic()) {
return typeName;
} else {
String className = anImport.getElement().getClassName();
if (className.contains("$")) {
return anImport.getElement().getPackageName() + "." +
className.substring(0, className.lastIndexOf('$'))
.replace('$', '.');
}
return anImport.getElement().getPackageName();
}
}
}
class Deserializer extends JsonDeserializer {
// TODO: verify deserialization with tests.
@Override
public ImportLayoutStyle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ImportLayoutStyle.Builder builder = ImportLayoutStyle.builder();
for (String currentField = null; p.hasCurrentToken() && p.getCurrentToken() != JsonToken.END_OBJECT; p.nextToken()) {
switch (p.currentToken()) {
case FIELD_NAME:
currentField = p.getCurrentName();
break;
case VALUE_STRING:
if ("layout".equals(currentField)) {
String block = p.getText().trim();
if ("".equals(block)) {
builder.blankLine();
} else if (block.startsWith("import ")) {
block = block.substring("import ".length());
if ("all other imports".equals(block)) {
builder.importAllOthers();
} else {
boolean withSubpackages = !block.contains(" without subpackages");
block = withSubpackages ? block : block.substring(0, block.indexOf(" without subpackage"));
builder.importPackage(block, withSubpackages);
}
} else {
throw new IllegalArgumentException("Syntax error in layout block [" + block + "]");
}
} else if ("packagesToFold".equals(currentField)) {
String block = p.getText().trim();
if (block.startsWith("import ")) {
block = block.substring("import ".length());
boolean withSubpackages = !block.contains(" without subpackages");
block = withSubpackages ? block : block.substring(0, block.indexOf(" without subpackage"));
builder.packageToFold(block, withSubpackages);
}
} else {
break;
}
break;
case VALUE_NUMBER_INT:
if ("topLevelSymbolsToUseStarImport".equals(currentField)) {
builder.topLevelSymbolsToUseStarImport(p.getValueAsInt());
} else if ("javaStaticsAndEnumsToUseStarImport".equals(currentField)) {
builder.javaStaticsAndEnumsToUseStarImport(p.getValueAsInt());
}
break;
}
}
return builder.build();
}
}
class Serializer extends JsonSerializer {
// TODO: verify serialization with tests.
@Override
public void serializeWithType(ImportLayoutStyle value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
WritableTypeId typeId = typeSer.typeId(value, JsonToken.START_OBJECT);
typeSer.writeTypePrefix(gen, typeId);
serializeFields(value, gen);
typeSer.writeTypeSuffix(gen, typeId);
}
@Override
public void serialize(ImportLayoutStyle value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
serializeFields(value, gen);
gen.writeEndObject();
}
private void serializeFields(ImportLayoutStyle value, JsonGenerator gen) throws IOException {
gen.writeNumberField("topLevelSymbolsToUseStarImport", value.getTopLevelSymbolsToUseStarImport());
gen.writeNumberField("javaStaticsAndEnumsToUseStarImport", value.getJavaStaticsAndEnumsToUseStarImport());
@SuppressWarnings("SuspiciousToArrayCall") String[] blocks = value.getLayout().stream()
.map(block -> {
if (block instanceof ImportLayoutStyle.Block.BlankLines) {
return "";
} else if (block instanceof ImportLayoutStyle.Block.AllOthers) {
return "import all other imports";
} else if (block instanceof ImportLayoutStyle.Block.ImportPackage) {
ImportLayoutStyle.Block.ImportPackage importPackage = (ImportLayoutStyle.Block.ImportPackage) block;
String withSubpackages = importPackage.getPackageWildcard().pattern().contains("[^.]+") ? " without subpackages" : "";
return "import " + importPackage.getPackageWildcard().pattern()
.replace("\\.", ".")
.replace(".+", "*")
.replace("[^.]+", "*") + withSubpackages;
}
return new UnsupportedOperationException("Unknown block type " + block.getClass().getName());
})
.toArray(String[]::new);
@SuppressWarnings("SuspiciousToArrayCall") String[] packagesToFold = value.getPackagesToFold().stream()
.map(block -> {
if (block instanceof ImportLayoutStyle.Block.ImportPackage) {
ImportLayoutStyle.Block.ImportPackage importPackage = (ImportLayoutStyle.Block.ImportPackage) block;
String withSubpackages = importPackage.getPackageWildcard().pattern().contains("[^.]+") ? " without subpackages" : "";
return "import " + importPackage.getPackageWildcard().pattern()
.replace("\\.", ".")
.replace(".+", "*")
.replace("[^.]+", "*") + withSubpackages;
}
return new UnsupportedOperationException("Unknown block type " + block.getClass().getName());
})
.toArray(String[]::new);
gen.writeArrayFieldStart("layout");
gen.writeArray(blocks, 0, blocks.length);
gen.writeEndArray();
gen.writeArrayFieldStart("packagesToFold");
gen.writeArray(packagesToFold, 0, packagesToFold.length);
gen.writeEndArray();
}
}