View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.errorprone;
6   
7   import com.qulice.spi.Environment;
8   import java.util.ArrayList;
9   import java.util.List;
10  
11  /**
12   * The {@code javac} flags that pin the forked compiler to the Java source
13   * level of one batch of the project's sources.
14   *
15   * <p>Without them the forked {@code javac} runs at the host JDK's default
16   * language level (e.g. 21 on a JDK 21 host), so ErrorProne fires
17   * syntax-modernising checks such as {@code PatternMatchingInstanceof} even on
18   * projects that compile at {@code -source 8}, producing suggestions whose
19   * rewrite does not compile under the project's real source level. The level is
20   * derived exactly as {@code CheckstyleValidator} derives it:
21   * {@code maven.compiler.release} first, then {@code maven.compiler.source},
22   * accepting both the modern ({@code "8"}, {@code "17"}) and legacy
23   * ({@code "1.8"}) forms. When the project pins a {@code release},
24   * {@code --release} is forwarded; otherwise {@code -source}/{@code -target}
25   * are, which gates the language features while leaving the API surface
26   * untouched so that projects using newer library APIs under a plain
27   * {@code -source} build do not gain spurious "cannot find symbol" errors. When
28   * neither property is set the level is unknown and nothing is added, leaving
29   * the host default in place. See
30   * <a href="https://github.com/yegor256/qulice/issues/1716">#1716</a>.</p>
31   *
32   * <p>Maven does not oblige the main sources and the test sources to share a
33   * level: the {@code testCompile} goal reads {@code maven.compiler.testRelease},
34   * {@code maven.compiler.testSource} and {@code maven.compiler.testTarget}, so a
35   * project may compile its tests higher than its main code — as
36   * {@code hone-maven-plugin} does, staying usable from Java 8 while its own
37   * tests are written in Java 17. A test batch therefore derives its level from
38   * those properties first, and only when the project pins none of them does it
39   * fall back to the main ones, which keeps the fallback whole: a project that
40   * sets nothing test-specific is compiled exactly as before. See
41   * <a href="https://github.com/yegor256/qulice/issues/1746">#1746</a>.</p>
42   *
43   * <p>Which batches those are is not a matter of guessing at their names:
44   * {@link Batches} gives every declared test source root a batch of its own,
45   * named after the root, so the only batch that does not hold test sources is
46   * {@link Batches#MAIN}.</p>
47   *
48   * @since 1.0
49   */
50  final class Release {
51  
52      /**
53       * Environment to read the Maven properties from.
54       */
55      private final Environment env;
56  
57      /**
58       * Name of the batch being compiled.
59       */
60      private final String batch;
61  
62      /**
63       * Constructor.
64       *
65       * @param env Environment to read the Maven properties from
66       * @param batch Name of the batch being compiled
67       */
68      Release(final Environment env, final String batch) {
69          this.env = env;
70          this.batch = batch;
71      }
72  
73      /**
74       * The source-level flags to append to the {@code javac} command line.
75       *
76       * @return Source-level flags, possibly empty
77       */
78      List<String> flags() {
79          List<String> flags = new ArrayList<>(0);
80          if (!Batches.MAIN.equals(this.batch)) {
81              flags = this.pinned(
82                  "maven.compiler.testRelease",
83                  "maven.compiler.testSource",
84                  "maven.compiler.testTarget"
85              );
86          }
87          if (flags.isEmpty()) {
88              flags = this.pinned(
89                  "maven.compiler.release",
90                  "maven.compiler.source",
91                  "maven.compiler.target"
92              );
93          }
94          return flags;
95      }
96  
97      private List<String> pinned(
98          final String release, final String source, final String target
99      ) {
100         final List<String> flags = new ArrayList<>(4);
101         final int rel = Release.major(this.env.param(release, ""));
102         final int level = Release.major(this.env.param(source, ""));
103         if (rel >= 0) {
104             flags.add("--release");
105             flags.add(String.valueOf(rel));
106         } else if (level >= 0) {
107             flags.add("-source");
108             flags.add(String.valueOf(level));
109             flags.add("-target");
110             flags.add(String.valueOf(this.target(target, level)));
111         }
112         return flags;
113     }
114 
115     private int target(final String property, final int fallback) {
116         int level = Release.major(this.env.param(property, ""));
117         if (level < 0) {
118             level = fallback;
119         }
120         return level;
121     }
122 
123     private static int major(final String value) {
124         int result = -1;
125         if (value != null) {
126             String txt = value.trim();
127             if (txt.startsWith("1.")) {
128                 txt = txt.substring(2);
129             }
130             try {
131                 result = Integer.parseInt(txt);
132             } catch (final NumberFormatException ex) {
133                 result = -1;
134             }
135         }
136         return result;
137     }
138 }