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 }