Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ignore parse errors below imports (to allow the plugin to work with Java > 18) #83

Merged
merged 3 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/main/java/net/revelc/code/impsort/ImpSort.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ParserConfiguration.LanguageLevel;
import com.github.javaparser.Position;
import com.github.javaparser.Problem;
import com.github.javaparser.TokenRange;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
Expand Down Expand Up @@ -67,22 +68,24 @@ public class ImpSort {
private final boolean treatSamePackageAsUnused;
private final LineEnding lineEnding;
private final LanguageLevel languageLevel;
private final boolean ignoreParseErrorsBelowImports;

public ImpSort(final Charset sourceEncoding, final Grouper grouper, final boolean removeUnused,
final boolean treatSamePackageAsUnused, final LineEnding lineEnding) {
this(sourceEncoding, grouper, removeUnused, treatSamePackageAsUnused, lineEnding,
LanguageLevel.POPULAR);
LanguageLevel.POPULAR, false);
}

public ImpSort(final Charset sourceEncoding, final Grouper grouper, final boolean removeUnused,
final boolean treatSamePackageAsUnused, final LineEnding lineEnding,
final LanguageLevel languageLevel) {
final LanguageLevel languageLevel, final boolean ignoreParseErrorsBelowImports) {
this.sourceEncoding = sourceEncoding;
this.grouper = grouper;
this.removeUnused = removeUnused;
this.treatSamePackageAsUnused = treatSamePackageAsUnused;
this.lineEnding = lineEnding;
this.languageLevel = languageLevel;
this.ignoreParseErrorsBelowImports = ignoreParseErrorsBelowImports;
}

private static List<String> readAllLines(String str) {
Expand Down Expand Up @@ -129,16 +132,20 @@ public Result parseFile(final Path path, final byte[] buf) throws IOException {
parseResult.getProblems().forEach(System.out::println);
return new ImpSortException(path, Reason.UNABLE_TO_PARSE);
});
if (!parseResult.isSuccessful()) {
parseResult.getProblems().forEach(System.out::println);
List<Problem> reportableProblems = ignoreParseErrorsBelowImports
? ParseProblemFilter.getProblemsAboveFirstTopLevelDeclaration(unit,
parseResult.getProblems())
: parseResult.getProblems();
if (!parseResult.isSuccessful() && !reportableProblems.isEmpty()) {
reportableProblems.forEach(System.out::println);
throw new ImpSortException(path, Reason.PARTIAL_PARSE);
}
Position packagePosition = unit.getPackageDeclaration().map(p -> p.getEnd().orElseThrow())
.orElse(unit.getBegin().orElseThrow());
NodeList<ImportDeclaration> importDeclarations = unit.getImports();
if (importDeclarations.isEmpty()) {
return new Result(path, sourceEncoding, fileLines, 0, fileLines.size(), "", "",
Collections.emptyList(), impLineEnding);
Collections.emptyList(), impLineEnding, parseResult.getProblems(), reportableProblems);
}

// find orphaned comments before between package and last import
Expand Down Expand Up @@ -189,7 +196,7 @@ public Result parseFile(final Path path, final byte[] buf) throws IOException {
}

return new Result(path, sourceEncoding, fileLines, start, stop, originalSection, newSection,
allImports, impLineEnding);
allImports, impLineEnding, parseResult.getProblems(), reportableProblems);
}

// return imports, with associated comments, in order found in the file
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/net/revelc/code/impsort/ParseProblemFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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
*
* http://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 net.revelc.code.impsort;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import com.github.javaparser.*;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.TypeDeclaration;

public class ParseProblemFilter {

/**
* Only return problems above the first top level declaration (if any, otherwise return all). All
* other problems might for example be caused be a newer Java version with not yet known syntax
* but should have no impact on import parsing.
*/
public static List<Problem> getProblemsAboveFirstTopLevelDeclaration(CompilationUnit unit,
List<Problem> problems) {
Optional<Position> firstTopLevelBegin = unit.getTypes().stream().map(TypeDeclaration::getBegin)
.filter(Optional::isPresent).map(Optional::get).sorted().findFirst();
// in case of no top level declaration per definition all problems are above ;-)
if (firstTopLevelBegin.isEmpty()) {
return problems;
}

return problems.stream()
.filter(problem -> toRange(problem)
.map(problemRange -> problemRange.isBefore(firstTopLevelBegin.orElseThrow()))
.orElse(Boolean.TRUE))
.collect(Collectors.toList());
}

public static Optional<Range> toRange(Problem problem) {
return problem.getLocation().map(TokenRange::getBegin).flatMap(JavaToken::getRange);
}
}
28 changes: 26 additions & 2 deletions src/main/java/net/revelc/code/impsort/Result.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
import java.util.Collections;
import java.util.List;

import com.github.javaparser.Position;
import com.github.javaparser.Problem;

public class Result {

private Boolean isSorted;
Expand All @@ -41,13 +44,15 @@ public class Result {
private final int start;
private final int stop;
private final LineEnding lineEnding;
private List<Problem> problems;
private List<Problem> reportableProblems;

public static final Result EMPTY_FILE =
new Result(null, null, null, 0, 0, "", "", Collections.emptyList(), null);
new Result(null, null, null, 0, 0, "", "", Collections.emptyList(), null, null, null);

Result(Path path, Charset sourceEncoding, List<String> fileLines, int start, int stop,
String originalSection, String newSection, Collection<Import> allImports,
LineEnding lineEnding) {
LineEnding lineEnding, List<Problem> problems, List<Problem> reportableProblems) {
this.path = path;
this.sourceEncoding = sourceEncoding;
this.originalSection = originalSection;
Expand All @@ -57,6 +62,8 @@ public class Result {
this.start = start;
this.stop = stop;
this.lineEnding = lineEnding;
this.problems = problems;
this.reportableProblems = reportableProblems;
}

public boolean isSorted() {
Expand Down Expand Up @@ -109,4 +116,21 @@ private byte[] writeLines(Path destination, List<String> lines, Charset sourceEn
return buf;
}

public Position getFirstLineContaining(String string) {
for (int i = 0; i < fileLines.size(); i++) {
if (fileLines.get(i).contains(string)) {
return new Position(i, 0);
}
}

throw new IllegalStateException("Can't find '" + string + "'!");
}

public List<Problem> getProblems() {
return Collections.unmodifiableList(problems);
}

public List<Problem> getReportableProblems() {
return Collections.unmodifiableList(reportableProblems);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -233,10 +234,33 @@ abstract class AbstractImpSortMojo extends AbstractMojo {
@Parameter(defaultValue = "${project.build.directory}", property = "impsort.cachedir")
private File cachedir;

/**
* Allows to ignore parse errors below the imports
*
* <p>
* Whilst parsing all Java files completely only the parts above the first top level decleration
* are relevant for sorting the imports. This property allows to ignore all parse errors which are
* located under the first top level decleration. This allows import sorting with future Java
* versions that allow syntax not (yet) supported by the used Java parser.
*
* <b>Warning:</b> This is incompatible with <code>removeUnused=true</code> because parse errors
* might cause false positives and actually used imports would be removed.
*
* @since 1.10.0
*/
@Parameter(property = "impsort.ignoreParseErrorsBelowImports", defaultValue = "false")
private boolean ignoreParseErrorsBelowImports;

abstract byte[] processResult(Path path, Result results) throws MojoFailureException;

@Override
public final void execute() throws MojoExecutionException, MojoFailureException {
if (removeUnused && ignoreParseErrorsBelowImports) {
fail(
"ignoreParseErrorsBelowImports=true can't be used together with removeUnused=true because parse errors might "
+ "cause false positives and actually used imports would be removed!");
}

if (skip) {
getLog().info("Skipping execution of impsort-maven-plugin");
return;
Expand All @@ -261,10 +285,10 @@ public final void execute() throws MojoExecutionException, MojoFailureException
breadthFirstComparator);
Charset encoding = Charset.forName(sourceEncoding);

LanguageLevel langLevel = getLanguageLevel(compliance);
LanguageLevel langLevel = getLanguageLevel(compliance, ignoreParseErrorsBelowImports);
getLog().debug("Using compiler compliance level: " + langLevel.toString());
ImpSort impSort = new ImpSort(encoding, grouper, removeUnused, treatSamePackageAsUnused,
lineEnding, langLevel);
lineEnding, langLevel, ignoreParseErrorsBelowImports);
AtomicLong numAlreadySorted = new AtomicLong(0);
AtomicLong numProcessed = new AtomicLong(0);

Expand Down Expand Up @@ -362,7 +386,7 @@ protected void fail(String message, Throwable cause) throws MojoFailureException
: new MojoFailureException(message, cause);
}

static LanguageLevel getLanguageLevel(String compliance) {
static LanguageLevel getLanguageLevel(String compliance, boolean ignoreParseErrorsBelowImports) {
if (compliance == null || compliance.trim().isEmpty()) {
return LanguageLevel.POPULAR;
}
Expand All @@ -375,7 +399,17 @@ static LanguageLevel getLanguageLevel(String compliance) {
} else {
langLevel = "JAVA_" + v;
}
return LanguageLevel.valueOf(langLevel);

return parseLanguageLevel(langLevel, ignoreParseErrorsBelowImports);
}

private static LanguageLevel parseLanguageLevel(String langLevel,
boolean ignoreParseErrorsBelowImports) {
return Stream.of(LanguageLevel.values()).filter(ll -> ll.name().equals(langLevel)).findFirst()
.or(() -> ignoreParseErrorsBelowImports ? Optional.of(LanguageLevel.POPULAR)
: Optional.empty())
.orElseThrow(() -> new IllegalArgumentException("No enum constant "
+ LanguageLevel.class.getName().replace('$', '.') + "." + langLevel));
}

/**
Expand Down
28 changes: 27 additions & 1 deletion src/test/java/net/revelc/code/impsort/ImpSortTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public void testJava14PreviewFeatures() throws IOException {
Path p =
Paths.get(System.getProperty("user.dir"), "src", "test", "resources", "Java14Preview.java");
Result result = new ImpSort(StandardCharsets.UTF_8, eclipseDefaults, true, true,
LineEnding.AUTO, LanguageLevel.JAVA_14_PREVIEW).parseFile(p);
LineEnding.AUTO, LanguageLevel.JAVA_14_PREVIEW, false).parseFile(p);

Path output = File.createTempFile("java14preview", null, new File("target")).toPath();
result.saveSorted(output);
Expand All @@ -235,5 +235,31 @@ public void testJava14PreviewFeatures() throws IOException {
lines.stream().filter(line -> line.contains("public record Java14Preview")).count());
}

@Test
public void testIgnoreParseErrorsBelowImports() throws IOException {
Path p = Paths.get(System.getProperty("user.dir"), "src", "test", "resources",
"Java21RecordDeconstruction.java");

Result result = new ImpSort(StandardCharsets.UTF_8, eclipseDefaults, true, true,
LineEnding.AUTO, LanguageLevel.JAVA_18, true).parseFile(p);

assertTrue(result.getReportableProblems().isEmpty());
assertEquals(1,
result.getProblems().stream().map(ParseProblemFilter::toRange)
.filter(problemRange -> problemRange.orElseThrow()
.isAfter(result.getFirstLineContaining("public record Java21RecordDeconstruction")))
.count());

Path output =
File.createTempFile("java21record-deconstruction", null, new File("target")).toPath();
result.saveSorted(output);

List<String> lines = Files.readAllLines(output);
// check that the deconstruction hasn't been mangled (even if not parsable)
assertEquals(1,
lines.stream()
.filter(line -> line.contains("obj instanceof Java21RecordDeconstruction(Point point)"))
.count());
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ public class AbstractImpSortMojoTest {

@Test
public void testLanguageLevel() {
assertSame(LanguageLevel.POPULAR, getLanguageLevel(null));
assertSame(LanguageLevel.POPULAR, getLanguageLevel(""));
assertSame(LanguageLevel.POPULAR, getLanguageLevel(null, false));
assertSame(LanguageLevel.POPULAR, getLanguageLevel("", false));

assertSame(LanguageLevel.POPULAR, getLanguageLevel(null, true));
assertSame(LanguageLevel.POPULAR, getLanguageLevel("", true));
assertSame(LanguageLevel.POPULAR, getLanguageLevel("42", true));

assertSame(LanguageLevel.JAVA_12, getLanguageLevel("12", true));

var prefix = "JAVA_";
var previewSuffix = "_PREVIEW";
// only these can be represented using single digit or 1.<digit>, as in Java 5 or Java 1.5
Expand All @@ -44,14 +51,14 @@ public void testLanguageLevel() {
if (version.length() == 1) {
// check that the versions that can be represented by either X or 1.X are the same
assertTrue(expectedOneDotOrSingle.remove(version), "Unexpectedly saw " + version);
assertSame(level, getLanguageLevel("1." + version));
assertSame(level, getLanguageLevel("1." + version, false));
} else if (!version.contains(".") && !version.contains(previewSuffix)) {
// make sure versions above Java 9 can't be represented as 1.X, as in 1.10
final var v = version;
assertThrows(IllegalArgumentException.class, () -> getLanguageLevel("1." + v));
assertThrows(IllegalArgumentException.class, () -> getLanguageLevel("1." + v, false));
}
// basic check to make sure our getter method returns the correct level
assertSame(level, getLanguageLevel(version));
assertSame(level, getLanguageLevel(version, false));
});
assertTrue(expectedOneDotOrSingle.isEmpty(), "Did not encounter " + expectedOneDotOrSingle);
}
Expand Down
27 changes: 27 additions & 0 deletions src/test/resources/Java21RecordDeconstruction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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
*
* http://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 some.pkg;

import java.awt.Point;

public record Java21RecordDeconstruction(Point point) {

public static int getX(Object obj) {
if (obj instanceof Java21RecordDeconstruction(Point point)) {
return point.x;
} else {
throw new IllegalArgumentException();
}
}
}