diff --git a/CHANGES.md b/CHANGES.md index f6671d2191..634542e6af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `importOrder` now support groups of imports without blank lines ([#1401](https://github.com/diffplug/spotless/pull/1401)) ### Fixed * Don't treat `@Value` as a type annotation [#1367](https://github.com/diffplug/spotless/pull/1367) * Support `ktlint_disabled_rules` in `ktlint` 0.47.x [#1378](https://github.com/diffplug/spotless/pull/1378) diff --git a/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java b/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java index ec075ffedd..0390d99c1b 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ import java.io.Serializable; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; @@ -25,12 +27,41 @@ // which itself is licensed under the Apache 2.0 license. final class ImportSorterImpl { - private final List template = new ArrayList<>(); + private static final String CATCH_ALL_SUBGROUP = ""; + private static final String STATIC_KEYWORD = "static "; + private static final String STATIC_SYMBOL = "\\#"; + private static final String SUBGROUP_SEPARATOR = "|"; + + private final List importsGroups; private final Map> matchingImports = new HashMap<>(); private final List notMatching = new ArrayList<>(); private final Set allImportOrderItems = new HashSet<>(); private final Comparator ordering; + // An ImportsGroup is a group of imports ; each group is separated by blank lines. + // A group is composed of subgroups : imports are sorted by subgroup. + private static class ImportsGroup { + + private final List subGroups; + + public ImportsGroup(String importOrder) { + this.subGroups = Stream.of(importOrder.split("\\" + SUBGROUP_SEPARATOR)) + .map(this::normalizeStatic) + .collect(Collectors.toList()); + } + + private String normalizeStatic(String subgroup) { + if (subgroup.startsWith(STATIC_SYMBOL)) { + return subgroup.replace(STATIC_SYMBOL, STATIC_KEYWORD); + } + return subgroup; + } + + public List getSubGroups() { + return subGroups; + } + } + static List sort(List imports, List importsOrder, boolean wildcardsLast, String lineFormat) { ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast); return importsSorter.sort(imports, lineFormat); @@ -40,43 +71,42 @@ private List sort(List imports, String lineFormat) { filterMatchingImports(imports); mergeNotMatchingItems(false); mergeNotMatchingItems(true); - mergeMatchingItems(); + List sortedImported = mergeMatchingItems(); - return getResult(lineFormat); + return getResult(sortedImported, lineFormat); } private ImportSorterImpl(List importOrder, boolean wildcardsLast) { - List importOrderCopy = new ArrayList<>(importOrder); - normalizeStaticOrderItems(importOrderCopy); - putStaticItemIfNotExists(importOrderCopy); - template.addAll(importOrderCopy); + importsGroups = importOrder.stream().filter(Objects::nonNull).map(ImportsGroup::new).collect(Collectors.toList()); + putStaticItemIfNotExists(importsGroups); + putCatchAllGroupIfNotExists(importsGroups); + ordering = new OrderingComparator(wildcardsLast); - this.allImportOrderItems.addAll(importOrderCopy); + + List subgroups = importsGroups.stream().map(ImportsGroup::getSubGroups).flatMap(Collection::stream).collect(Collectors.toList()); + this.allImportOrderItems.addAll(subgroups); } - private static void putStaticItemIfNotExists(List allImportOrderItems) { - boolean contains = false; + private void putStaticItemIfNotExists(List importsGroups) { + boolean catchAllSubGroupExist = importsGroups.stream().anyMatch(group -> group.getSubGroups().contains(STATIC_KEYWORD)); + if (catchAllSubGroupExist) { + return; + } + int indexOfFirstStatic = 0; - for (int i = 0; i < allImportOrderItems.size(); i++) { - String allImportOrderItem = allImportOrderItems.get(i); - if (allImportOrderItem.equals("static ")) { - contains = true; - } - if (allImportOrderItem.startsWith("static ")) { + for (int i = 0; i < importsGroups.size(); i++) { + boolean subgroupMatch = importsGroups.get(i).getSubGroups().stream().anyMatch(subgroup -> subgroup.startsWith(STATIC_KEYWORD)); + if (subgroupMatch) { indexOfFirstStatic = i; } } - if (!contains) { - allImportOrderItems.add(indexOfFirstStatic, "static "); - } + importsGroups.add(indexOfFirstStatic, new ImportsGroup(STATIC_KEYWORD)); } - private static void normalizeStaticOrderItems(List allImportOrderItems) { - for (int i = 0; i < allImportOrderItems.size(); i++) { - String s = allImportOrderItems.get(i); - if (s.startsWith("\\#")) { - allImportOrderItems.set(i, s.replace("\\#", "static ")); - } + private void putCatchAllGroupIfNotExists(List importsGroups) { + boolean catchAllSubGroupExist = importsGroups.stream().anyMatch(group -> group.getSubGroups().contains(CATCH_ALL_SUBGROUP)); + if (!catchAllSubGroupExist) { + importsGroups.add(new ImportsGroup(CATCH_ALL_SUBGROUP)); } } @@ -87,9 +117,7 @@ private void filterMatchingImports(List imports) { for (String anImport : imports) { String orderItem = getBestMatchingImportOrderItem(anImport); if (orderItem != null) { - if (!matchingImports.containsKey(orderItem)) { - matchingImports.put(orderItem, new ArrayList<>()); - } + matchingImports.computeIfAbsent(orderItem, key -> new ArrayList<>()); matchingImports.get(orderItem).add(anImport); } else { notMatching.add(anImport); @@ -116,34 +144,14 @@ private void filterMatchingImports(List imports) { * not matching means it does not match any order item, so it will be appended before or after order items */ private void mergeNotMatchingItems(boolean staticItems) { - sort(notMatching); - - int firstIndexOfOrderItem = getFirstIndexOfOrderItem(notMatching, staticItems); - int indexOfOrderItem = 0; for (String notMatchingItem : notMatching) { if (!matchesStatic(staticItems, notMatchingItem)) { continue; } boolean isOrderItem = isOrderItem(notMatchingItem, staticItems); - if (isOrderItem) { - indexOfOrderItem = template.indexOf(notMatchingItem); - } else { - if (indexOfOrderItem == 0 && firstIndexOfOrderItem != 0) { - // insert before alphabetically first order item - template.add(firstIndexOfOrderItem, notMatchingItem); - firstIndexOfOrderItem++; - } else if (firstIndexOfOrderItem == 0) { - // no order is specified - if (template.size() > 0 && (template.get(template.size() - 1).startsWith("static"))) { - // insert N after last static import - template.add(ImportSorter.N); - } - template.add(notMatchingItem); - } else { - // insert after the previous order item - template.add(indexOfOrderItem + 1, notMatchingItem); - indexOfOrderItem++; - } + if (!isOrderItem) { + matchingImports.computeIfAbsent(CATCH_ALL_SUBGROUP, key -> new ArrayList<>()); + matchingImports.get(CATCH_ALL_SUBGROUP).add(notMatchingItem); } } } @@ -153,76 +161,44 @@ private boolean isOrderItem(String notMatchingItem, boolean staticItems) { return contains && matchesStatic(staticItems, notMatchingItem); } - /** - * gets first order item from sorted input list, and finds out it's index in template. - */ - private int getFirstIndexOfOrderItem(List notMatching, boolean staticItems) { - int firstIndexOfOrderItem = 0; - for (String notMatchingItem : notMatching) { - if (!matchesStatic(staticItems, notMatchingItem)) { - continue; - } - boolean isOrderItem = isOrderItem(notMatchingItem, staticItems); - if (isOrderItem) { - firstIndexOfOrderItem = template.indexOf(notMatchingItem); - break; - } - } - return firstIndexOfOrderItem; - } - private static boolean matchesStatic(boolean staticItems, String notMatchingItem) { - boolean isStatic = notMatchingItem.startsWith("static "); + boolean isStatic = notMatchingItem.startsWith(STATIC_KEYWORD); return (isStatic && staticItems) || (!isStatic && !staticItems); } - private void mergeMatchingItems() { - for (int i = 0; i < template.size(); i++) { - String item = template.get(i); - if (allImportOrderItems.contains(item)) { - // find matching items for order item - List strings = matchingImports.get(item); + private List mergeMatchingItems() { + List template = new ArrayList<>(); + for (ImportsGroup group : importsGroups) { + boolean groupIsNotEmpty = false; + for (String subgroup : group.getSubGroups()) { + List strings = matchingImports.get(subgroup); if (strings == null || strings.isEmpty()) { - // if there is none, just remove order item - template.remove(i); - i--; continue; } + groupIsNotEmpty = true; List matchingItems = new ArrayList<>(strings); sort(matchingItems); - - // replace order item by matching import statements - // this is a mess and it is only a luck that it works :-] - template.remove(i); - if (i != 0 && !template.get(i - 1).equals(ImportSorter.N)) { - template.add(i, ImportSorter.N); - i++; - } - if (i + 1 < template.size() && !template.get(i + 1).equals(ImportSorter.N) - && !template.get(i).equals(ImportSorter.N)) { - template.add(i, ImportSorter.N); - } - template.addAll(i, matchingItems); - if (i != 0 && !template.get(i - 1).equals(ImportSorter.N)) { - template.add(i, ImportSorter.N); - } - + template.addAll(matchingItems); + } + if (groupIsNotEmpty) { + template.add(ImportSorter.N); } } // if there is \n on the end, remove it - if (template.size() > 0 && template.get(template.size() - 1).equals(ImportSorter.N)) { + if (!template.isEmpty() && template.get(template.size() - 1).equals(ImportSorter.N)) { template.remove(template.size() - 1); } + return template; } private void sort(List items) { items.sort(ordering); } - private List getResult(String lineFormat) { + private List getResult(List sortedImported, String lineFormat) { List strings = new ArrayList<>(); - for (String s : template) { + for (String s : sortedImported) { if (s.equals(ImportSorter.N)) { strings.add(s); } else { diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 5acd85ddc8..b002c5bd74 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* `importOrder` now support groups of imports without blank lines ([#1401](https://github.com/diffplug/spotless/pull/1401)) ### Fixed * Don't treat `@Value` as a type annotation [#1367](https://github.com/diffplug/spotless/pull/1367) * Support `ktlint_disabled_rules` in `ktlint` 0.47.x [#1378](https://github.com/diffplug/spotless/pull/1378) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 7f5590da21..03d933aea2 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -151,8 +151,8 @@ spotless { // Use the default importOrder configuration importOrder() // optional: you can specify import groups directly - // note: you can use an empty string for all the imports you didn't specify explicitly, and '\\#` prefix for static imports - importOrder('java', 'javax', 'com.acme', '', '\\#com.acme', '\\#') + // note: you can use an empty string for all the imports you didn't specify explicitly, '|' to join group without blank line, and '\\#` prefix for static imports + importOrder('java|javax', 'com.acme', '', '\\#com.acme', '\\#') // optional: instead of specifying import groups directly you can specify a config file // export config file: https://github.com/diffplug/spotless/blob/main/ECLIPSE_SCREENSHOTS.md#creating-spotlessimportorder importOrderFile('eclipse-import-order.txt') // import order file as exported from eclipse diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index b08a2366fd..cd42f0e8d8 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `importOrder` now support groups of imports without blank lines ([#1401](https://github.com/diffplug/spotless/pull/1401)) ### Fixed * Don't treat `@Value` as a type annotation [#1367](https://github.com/diffplug/spotless/pull/1367) * Support `ktlint_disabled_rules` in `ktlint` 0.47.x [#1378](https://github.com/diffplug/spotless/pull/1378) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 2a249357a6..d596af229b 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -181,8 +181,8 @@ any other maven phase (i.e. compile) then it can be configured as below; false - java,javax,org,com,com.diffplug,,\\#com.diffplug,\\# - + java|javax,org,com,com.diffplug,,\\#com.diffplug,\\# + @@ -286,8 +286,8 @@ These mechanisms already exist for the Gradle plugin. - java,javax,org,com,com.diffplug,,\\#com.diffplug,\\# - + java|javax,org,com,com.diffplug,,\\#com.diffplug,\\# + diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroups.test b/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroups.test new file mode 100644 index 0000000000..b2346bb19c --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroups.test @@ -0,0 +1,16 @@ +import java.awt.*; +import java.lang.Runnable; +import java.lang.Thread; +import java.util.*; +import java.util.List; +import javax.annotation.Nullable; +import javax.inject.Inject; + +import org.dooda.Didoo; +import static com.foo.Bar; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; + +import static java.lang.Exception.*; +import static java.lang.Runnable.*; +import static org.hamcrest.Matchers.*; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedImportsSubgroups.test b/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedImportsSubgroups.test new file mode 100644 index 0000000000..35d8c465e4 --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedImportsSubgroups.test @@ -0,0 +1,15 @@ +import static java.lang.Exception.*; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import org.dooda.Didoo; +import java.util.List; +import javax.inject.Inject; +import java.lang.Thread; +import java.util.*; +import java.lang.Runnable; +import static org.hamcrest.Matchers.*; +import javax.annotation.Nullable; + +import static java.lang.Runnable.*; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.foo.Bar +import java.awt.*; diff --git a/testlib/src/test/java/com/diffplug/spotless/java/ImportOrderStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/ImportOrderStepTest.java index 332b5bc979..32cf0a97c0 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/ImportOrderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/ImportOrderStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,12 @@ void sortImportsFromArray() throws Throwable { assertOnResources(step, "java/importsorter/JavaCodeUnsortedImports.test", "java/importsorter/JavaCodeSortedImports.test"); } + @Test + void sortImportsFromArrayWithSubgroups() throws Throwable { + FormatterStep step = ImportOrderStep.forJava().createFrom("java|javax", "org|\\#com", "\\#"); + assertOnResources(step, "java/importsorter/JavaCodeUnsortedImportsSubgroups.test", "java/importsorter/JavaCodeSortedImportsSubgroups.test"); + } + @Test void sortImportsFromFile() throws Throwable { FormatterStep step = ImportOrderStep.forJava().createFrom(createTestFile("java/importsorter/import.properties"));