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 com.qulice.spi.Relative;
9 import java.io.File;
10 import java.util.ArrayList;
11 import java.util.LinkedHashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.regex.Pattern;
15 import org.apache.commons.io.FilenameUtils;
16
17 /**
18 * Java sources split the way Maven compiles them: one batch per source
19 * root, never two roots in one batch.
20 *
21 * <p>A single {@code javac} pass over two roots would manufacture
22 * diagnostics no project could ever silence, because Maven lets every
23 * source root hold a twin of the same file. The clearest case is
24 * {@code package-info.java}, which Qulice's own {@code JavadocPackage}
25 * check demands in every package: with one pass, a package present in two
26 * roots earns {@code a package-info.java file has already been seen} and,
27 * once the file carries an annotation, {@code has already been
28 * annotated}.</p>
29 *
30 * <p>The roots a file may sit under are the ones the project declares,
31 * which is what {@link Environment#testdirs()} reports. Matching the
32 * {@code /src/test/} path fragment instead would miss every further root a
33 * project registers through
34 * {@code build-helper-maven-plugin:add-test-source} — {@code src/mock/java}
35 * in the jcabi parent POM (see
36 * <a href="https://github.com/yegor256/qulice/issues/1742">#1742</a>).
37 * Folding all of those roots into one {@code test} batch does not help
38 * either: {@code src/test/java} and {@code src/mock/java} are as free to
39 * hold twins as {@code src/main/java} and {@code src/test/java} are, so
40 * each root earns a batch of its own (see
41 * <a href="https://github.com/yegor256/qulice/issues/1745">#1745</a>). A
42 * file under none of them belongs to the main batch. Cross-batch
43 * references still resolve, since the project's own compiled classes are
44 * on the {@code -classpath} of every pass.</p>
45 *
46 * @since 1.0
47 */
48 public final class Batches {
49
50 /**
51 * Name of the batch of the sources that sit under no declared test
52 * source root. Every other batch this class makes holds test sources,
53 * which is how {@link Release} tells the two apart.
54 */
55 static final String MAIN = "main";
56
57 /**
58 * Runs of characters that have no place in a file name, in the path of
59 * a source root that {@link #label(File, int)} turns into a batch name.
60 */
61 private static final Pattern SEPARATOR = Pattern.compile("[^A-Za-z0-9]+");
62
63 /**
64 * The dashes {@link #SEPARATOR} leaves at either end of a batch name.
65 */
66 private static final Pattern EDGES = Pattern.compile("^-+|-+$");
67
68 /**
69 * Environment that knows the source roots.
70 */
71 private final Environment env;
72
73 /**
74 * Java source files to split.
75 */
76 private final List<File> sources;
77
78 /**
79 * Constructor.
80 *
81 * @param env Environment that knows the source roots
82 * @param sources Java source files to split
83 */
84 public Batches(final Environment env, final List<File> sources) {
85 this.env = env;
86 this.sources = sources;
87 }
88
89 /**
90 * Split the sources, one batch per source root.
91 *
92 * @return Batches by name, in compilation order, none of them empty
93 */
94 public Map<String, List<File>> split() {
95 final Map<String, String> roots = new LinkedHashMap<>(0);
96 int index = 0;
97 for (final File dir : this.env.testdirs()) {
98 index += 1;
99 roots.putIfAbsent(
100 Batches.slashed(dir).concat("/"), this.label(dir, index)
101 );
102 }
103 final Map<String, List<File>> batches = new LinkedHashMap<>(0);
104 batches.put(Batches.MAIN, new ArrayList<>(0));
105 for (final String name : roots.values()) {
106 batches.put(name, new ArrayList<>(0));
107 }
108 for (final File source : this.sources) {
109 batches.get(
110 Batches.owner(Batches.slashed(source), roots)
111 ).add(source);
112 }
113 batches.values().removeIf(List::isEmpty);
114 return batches;
115 }
116
117 private String label(final File root, final int index) {
118 return String.format(
119 "test-%d-%s",
120 index,
121 Batches.EDGES.matcher(
122 Batches.SEPARATOR.matcher(
123 new Relative(this.env.basedir(), root).path()
124 ).replaceAll("-")
125 ).replaceAll("")
126 );
127 }
128
129 private static String owner(final String path,
130 final Map<String, String> roots) {
131 String name = Batches.MAIN;
132 int longest = 0;
133 for (final Map.Entry<String, String> root : roots.entrySet()) {
134 final String dir = root.getKey();
135 if (dir.length() > longest && path.startsWith(dir)) {
136 longest = dir.length();
137 name = root.getValue();
138 }
139 }
140 return name;
141 }
142
143 private static String slashed(final File file) {
144 final String absolute = file.getAbsolutePath();
145 String path = FilenameUtils.normalize(absolute, true);
146 if (path == null) {
147 path = absolute.replace(File.separatorChar, '/');
148 }
149 return path;
150 }
151 }