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

Conversation

janScheible
Copy link
Contributor

  • unfortunately the used https://github.com/javaparser/javaparser is stuck with Java 18 (in latest version 3.25.7)
  • but it turned out that even in case of a parse error somewhere in a top level declaration the imports are correctly parsed and available
  • introduced <ignoreIrrelevantParseErrors> which is false for now but allows to ignore the parse errors in the top level declarations (they shouldn't affect import sorting at all)
  • introduced a test case with Java 21 record deconstruction

… > 18)

- unfortunately the used https://github.com/javaparser/javaparser is stuck with Java 18 (in latest version 3.25.7)
- but it turned out that even in case of a parse error somewhere in a top level declaration the imports are correctly parsed and available
- introduced <ignoreIrrelevantParseErrors> which is `false` for now but allows to ignore the parse errors in the top level declarations (they shouldn't affect import sorting at all)
- introduced a test case with Java 21 record deconstruction
@janScheible
Copy link
Contributor Author

To give it a bit more context: I had a general look how to make the plugin work with Java 21.
I also tried for example new PMD 7 parser. The PMD team proved to always be up-to-date with the latest Java version.
But that would of course be a bigger task to completely switch the parser.

Then I discovered that the imports are parsed despite parse errors for not yet known Java syntax.
If the ignore irrelevant errors approach proves to be okay then of course <ignoreIrrelevantParseErrors> can be removed again (perhaps after a certain period with setting it’s default to true).
For now it is only there to give that approach a try.

Copy link
Member

@ctubbsii ctubbsii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I think this is a great feature. Thanks for contributing it @janScheible ! I have some suggestions, and some questions.

In addition to those I said inline already, I'm curious how this interacts with the removeUnused. If parsing stops because of errors right away, does that cause the plugin to believe that all imports are unused, and cause all imports to be removed? We may want to warn, or explicitly prevent, the use of removeUnused option if this new option is used, if so.

src/test/resources/Java21RecordDeconstruction.java Outdated Show resolved Hide resolved
src/test/java/net/revelc/code/impsort/ImpSortTest.java Outdated Show resolved Hide resolved
Comment on lines 38 to 39
// should not happen but let's simply give up and return the problems as they are
return problems;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could happen if a file contained only imports, but no class declaration, right?

What happens if the error is in the package declaration above the imports? Would this feature just ignore the imports that come after?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tested it. The parser is happy with a file without a top level declaration. I changed the comment in the code accordingly.

If the package declaration would cause a parse error the plugin would still fail (as it did before my changes).

src/main/java/net/revelc/code/impsort/ImpSort.java Outdated Show resolved Hide resolved
Comment on lines 382 to 383
static LanguageLevel getLanguageLevel(String compliance, boolean ignoreIrrelevantParseErrors) {
if (compliance == null || compliance.trim().isEmpty() || ignoreIrrelevantParseErrors) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why the presence of this new option means that the parser should ignore the user-specified compliance level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivation is to just have <ignoreParseErrorsBelowImports>true</ignoreParseErrorsBelowImports> in the pom.xml and never look back. That means that Java versions that would otherwise make the plugin fail because the version is not yet known to the parser, must be dealt with (fall back to LanguageLevel.POPULAR).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's reasonable to fall back to the POPULAR one when one is specified that is not recognized (which guarantees errors) AND you have this option set.

However, it should not fall back to POPULAR and override the user configuration just because you have this option set, when there's a recognized version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly how it is implemented after my rework:

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));
}

… > 18)

- ignoreParseErrorsBelowImports is much more descriptive than ignoreIrrelevantParseErrors
- LanguageLevel.POPULAR is now only used when ignoreParseErrorsBelowImports=true and the used Java version would be unsupported by the parser
- Result now also contains the problems (mainly for the unit test)
@janScheible
Copy link
Contributor Author

In addition to those I said inline already, I'm curious how this interacts with the removeUnused. If parsing stops because of errors right away, does that cause the plugin to believe that all imports are unused, and cause all imports to be removed? We may want to warn, or explicitly prevent, the use of removeUnused option if this new option is used, if so.

You're absolutely right. It conflicts with removeUnused=true, I tested it with the unit tests. :-(
I now explicitly check for that case and let the plugin fail. I think that is better than just printing a warning. Warnings tend to be overlooked and then confuses the user. This is a pity but unfortunately unavoidable with the ignore approach. The only real solution would be a parser that supports the latest Java syntax.

@janScheible janScheible requested a review from ctubbsii December 8, 2023 21:14
@janScheible janScheible changed the title ignore irrelevant parse errors (to allow the plugin to work with Java > 18) ignore parse errors below imports (to allow the plugin to work with Java > 18) Dec 11, 2023
@hazendaz
Copy link
Member

Am I missing something here? If you take your project and run with java 21 as a build time, it works fine now. The moment you stay I want to target jdk 19 or above runtime, it fails enum check and javaparser then does not work at all. So what exactly is this doing for me? Just ignoring it? I can skip the plugin and already started globally doing so. As long as Javaparser team is so reluctant to just let the enums be and work its a stalemate in my opinion and I don't see why trying to do something like this is beneficial that I don't get by already turning it off. And lets face it, teams that are targeting these higher jdks are certainly not the ones that need help making sure their code is formatted.

I'm sure I'm missing something here but that is my take. Do note, I actually started working with Javaparser team due to this and have already had many PRs merged but they want actual logic for jdk 18 preview, and all for 19, 20, 21, and 22 before any of that is even allowed but at same time said they lost most contributors to help and I also don't have time either. If you really want impsort on those higher versions, just use spotless and possibly open rewrite. Or better yet, go to the source and help them get it compliant.

@janScheible
Copy link
Contributor Author

Summary of my understanding of the problem:

  • currently (1.9.0) the plugin fails with a Java 21 build (setting <compliance> to Java 17 is not really helpful because then the used java-parser fails with Java 17+ syntax like for example record deconstruction)
  • disabling the plugin completely is not a real solution for me because I use it together with spring-javaformat-maven-plugin (which does not touch the imports)
  • the core problem is of course the used java-parser project that is stuck with Java 17 (resp. 18 in 3.25.7)
    • I can understand that the java-parser team does not want to just add the enum because then the java-parser would still fail with Java 18+ syntax like for example record deconstruction

What I intend to achieve with my pull request:

  • (a) make the plugin work with Java 18+

Limitations of my pull request:

  • unfortunate side-effect that a reliable <removeUnused> is not possible anymore (and now disabled in the code)
  • does not solve the root-cause, which is java-parser being stuck with Java 17/18

Potential solutions:

  • (b) contribute to java-parser to add support for newer Java versions
  • (c) switch the plugin to a different Java parser
    • needs IMHO a refactoring first, which decouples the class ImpSort from the used parser (by introducing some java-parser independent interface and a java-parser implementation as the first step)
  • (d) like @hazendaz suggested switch to a different plugin like spotless or OpenRewrite

From a user's point of view I think that doing nothing would be really bad for the plugin. Users will be forced to either turn it off and/or switch to another solution. My pull request (a) has the advantage of being comparable simple.
It is really just a compromise between being completely unusable with Java 18+ and a lot more effort to fix the root cause. Whilst (b) would be the perfect solution it is much more work. Also (c) might be a good option with medium effort (somewhere between (a) and (b)) but needs a good replacement for java-parser and commitment from the plugin team that working on a parser replacement is the way to go.

@hazendaz
Copy link
Member

@janScheible Thank you for incredible detail. This is helpful to understand full motivation. For my use cases I've went full on just disabling it and IMO that is a worst case scenario. I think this sounds ok now. Up to @ctubbsii to agree at this point and seems that was close, just a question that is outstanding you answered and has not been accepted yet. I went ahead and let the build run here.

@ctubbsii
Copy link
Member

I'm going to revisit this after the new year. I think there's some good stuff in here, but I want to examine the edge cases a bit more closely to understand the user experience under a few common scenarios. Thanks, @janScheible for putting all this work in to this so far.

@janScheible
Copy link
Contributor Author

I'm going to revisit this after the new year. I think there's some good stuff in here, but I want to examine the edge cases a bit more closely to understand the user experience under a few common scenarios. Thanks, @janScheible for putting all this work in to this so far.

Hi! Any news regarding the merge request?

@ctubbsii
Copy link
Member

ctubbsii commented Mar 5, 2024

@janScheible Sorry, not yet. I hope to get to it soon. In the meantime, can you resolve the conflicts?

@hazendaz
Copy link
Member

@janScheible Sorry, not yet. I hope to get to it soon. In the meantime, can you resolve the conflicts?

I took care of this conflict.

@hazendaz hazendaz added this to the 1.10.0 milestone Mar 11, 2024
@janScheible
Copy link
Contributor Author

janScheible commented Mar 13, 2024

I took care of this conflict.

Thank you @hazendaz. :-)

@hazendaz hazendaz dismissed ctubbsii’s stale review March 21, 2024 01:09

Issue was addressed with prevention from having both flags set.

@hazendaz
Copy link
Member

Gave my thumbs up here. I believe all outstanding concerns have been addressed but will wait a bit longer for @ctubbsii to get back on this.

@hazendaz hazendaz self-assigned this Apr 12, 2024
@hazendaz
Copy link
Member

I know @ctubbsii hasn't been around much lately. I see he is back doing some reviews last few days. I'll let this sit until Sunday. If no word by then I'd like to go ahead and merge and move for a release on this. Although I had hoped javaparser sped up some they really haven't and are still only to java 18 when I checked last week. So I think this is important as many are moving really fast to java 21.

@moradabadi
Copy link

@hazendaz Sunday has passed already, any chance to get it merged and release?

@hazendaz hazendaz merged commit bf56b7a into revelc:main Apr 27, 2024
3 checks passed
@hensan64
Copy link

hensan64 commented May 2, 2024

When will you deploy it to Maven Central?

@hazendaz
Copy link
Member

hazendaz commented May 5, 2024

@hensan64 At least another week. Been busy with other projects. @ctubbsii I can take care of this next weekend if you are unable to release it.

@hazendaz
Copy link
Member

taking care of the release in a few minutes.

@delanym
Copy link

delanym commented May 28, 2024

I'm still getting parser errors with v1.10.0

(line 1662,col 43) Parse error. Found  "ccm" <IDENTIFIER>, expected one of  "!=" "%" "&" "&&" "(" "*" "+" "," "-" "->" "/" ":" "<" "<=" "==" ">" ">=" "?" "^" "instanceof" "|" "||"                                                                        
Problem stacktrace :                                                                                                                                                                                                                                       
  com.github.javaparser.GeneratedJavaParser.generateParseException(GeneratedJavaParser.java:14041)                                                                                                                                                         
  com.github.javaparser.GeneratedJavaParser.jj_consume_token(GeneratedJavaParser.java:13886)                                                                                                                                                               
  com.github.javaparser.GeneratedJavaParser.SwitchEntry(GeneratedJavaParser.java:6443)                                                                                                                                                                     
  com.github.javaparser.GeneratedJavaParser.SwitchStatement(GeneratedJavaParser.java:6233)                                                                                                                                                                 
  com.github.javaparser.GeneratedJavaParser.Statement(GeneratedJavaParser.java:5683)                                                                                                                                                                       
  com.github.javaparser.GeneratedJavaParser.BlockStatement(GeneratedJavaParser.java:5933)                                                                                                                                                                  
  com.github.javaparser.GeneratedJavaParser.Statements(GeneratedJavaParser.java:2795)                                                                                                                                                                      
  com.github.javaparser.GeneratedJavaParser.Block(GeneratedJavaParser.java:5810)                                                                                                                                                                           
  com.github.javaparser.GeneratedJavaParser.MethodDeclaration(GeneratedJavaParser.java:2188)                                                                                                                                                               
  com.github.javaparser.GeneratedJavaParser.ClassOrInterfaceBodyDeclaration(GeneratedJavaParser.java:1785)                                                                                                                                                 
  com.github.javaparser.GeneratedJavaParser.ClassOrInterfaceBody(GeneratedJavaParser.java:1281)                                                                                                                                                            
  com.github.javaparser.GeneratedJavaParser.ClassOrInterfaceDeclaration(GeneratedJavaParser.java:538)                                                                                                                                                      
  com.github.javaparser.GeneratedJavaParser.CompilationUnit(GeneratedJavaParser.java:156)                                                                                                                                                                  
  com.github.javaparser.JavaParser.parse(JavaParser.java:125)                                                                                                                                                                                              
  com.github.javaparser.JavaParser.parse(JavaParser.java:305)                                                                                                                                                                                              
  net.revelc.code.impsort.ImpSort.parseFile(ImpSort.java:130)                                                                                                                                                                                              
  net.revelc.code.impsort.maven.plugin.AbstractImpSortMojo.lambda$execute$3(AbstractImpSortMojo.java:309)
        <plugin>
          <groupId>net.revelc.code</groupId>
          <artifactId>impsort-maven-plugin</artifactId>
          <version>1.10.0</version>
          <configuration>
            <removeUnused>true</removeUnused>
            <staticGroups>*</staticGroups>
            <groups>java.,javax.,org.,com.,*</groups>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>sort</goal>
              </goals>
            </execution>
          </executions>
        </plugin>

@delanym
Copy link

delanym commented May 28, 2024

Oh and impsort.compliance property set to 17

@janScheible
Copy link
Contributor Author

@delanym you have to add <ignoreParseErrorsBelowImports>true</ignoreParseErrorsBelowImports> to your <configuration>. But unfortunately this will not work together with <removeUnused>true</removeUnused> because finding unused imports needs the parsing that was skipped. You can for example use Checkstyle with <module name="UnusedImports"/> to make sure that there are no unused imports. See #83 (comment) for a detailed description.

@hazendaz
Copy link
Member

@delanym One other thing, if you set impsort.compliance (assuming in properties section) to 17 and your code failed, you likely have a bad class that you should look at or introduced code > 17 that cannot work. The plugin on version 1.9.0 worked fine with java 21 even targeting java 21 so long as the compliance level was set to 17 and there was in fact no java 21 code being used. As @janScheible noted, if you have in fact added java 19+ (javaparser supports to 18 in 1.10.0), then you need the other flag as it is not a default of true.

@delanym
Copy link

delanym commented May 28, 2024

I am using 21 language constructs. I'm sorry to say it still fails to parse with this config:

    <properties>
      <impsort.compliance>21</impsort.compliance>
    </properties>

        <plugin>
          <groupId>net.revelc.code</groupId>
          <artifactId>impsort-maven-plugin</artifactId>
          <version>1.10.0</version>
          <configuration>
            <ignoreParseErrorsBelowImports>true</ignoreParseErrorsBelowImports>
            <!--            <removeUnused>true</removeUnused>-->
            <staticGroups>*</staticGroups>
            <groups>java.,javax.,org.,com.,*</groups>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>sort</goal>
              </goals>
            </execution>
          </executions>
        </plugin>

@delanym
Copy link

delanym commented May 28, 2024

Be nice if I could ask javac --whatversion file.java. Answering such a simple question requires trial and error

@delanym
Copy link

delanym commented May 29, 2024

I should mention the plugin sorts import statements in files in earlier modules before failing on the same file every time in a subsequent reactor module. So I tried skipping that module, but then the fail occurs in the same way on another module further down the DAG. Proprietary code. There's nothing particularly striking about these files though.

@hazendaz
Copy link
Member

hazendaz commented Jun 4, 2024

Will be releasing this again tomorrow as javaparser has now released java 21 support.

In the meantime, try overriding javaparser to the latest release as a dependency to the plugin. See if that fixes the issue. If not, could you possibly create a small reproducible project to cause the issue?

@moradabadi
Copy link

@hazendaz It works.

<plugin>
    <groupId>net.revelc.code</groupId>
    <artifactId>impsort-maven-plugin</artifactId>
    <version>1.10.0</version>
    <configuration>
        <compliance>21</compliance>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>com.github.javaparser</groupId>
            <artifactId>javaparser-core</artifactId>
            <version>3.26.0</version>
        </dependency>
    </dependencies>
</plugin>

Without the javaparser's latest version it fails in this kind of code:

var person = switch (myEnum){
    case A, B -> new Person("A B");
    case null, default -> new Person("Default");
};

Can we have a new version please?

@hazendaz
Copy link
Member

hazendaz commented Jun 5, 2024 via email

@delanym
Copy link

delanym commented Jun 5, 2024

Javaparser 3.26.0 can't handle a normal switch statement and its killing impsort plugin.

@hazendaz
Copy link
Member

hazendaz commented Jun 5, 2024

Javaparser 3.26.0 can't handle a normal switch statement and its killing impsort plugin.

Linking javaparser ticket javaparser/javaparser#4455

@hazendaz
Copy link
Member

new release 1.11.0 has been pushed to central.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants