1
2
3
4
5 package com.qulice.maven;
6
7 import com.google.common.base.Predicate;
8 import com.google.common.base.Predicates;
9 import com.google.common.collect.Collections2;
10 import com.jcabi.log.Logger;
11 import com.qulice.spi.ValidationException;
12 import java.io.File;
13 import java.io.IOException;
14 import java.nio.charset.StandardCharsets;
15 import java.nio.file.Files;
16 import java.nio.file.Path;
17 import java.nio.file.Paths;
18 import java.util.Collection;
19 import java.util.Enumeration;
20 import java.util.HashSet;
21 import java.util.LinkedList;
22 import java.util.Set;
23 import java.util.jar.JarEntry;
24 import java.util.jar.JarFile;
25 import java.util.stream.Stream;
26 import org.apache.maven.artifact.Artifact;
27 import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
28 import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzer;
29 import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzerException;
30 import org.cactoos.text.Joined;
31 import org.codehaus.plexus.PlexusConstants;
32 import org.codehaus.plexus.PlexusContainer;
33 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
34 import org.codehaus.plexus.context.ContextException;
35
36
37
38
39
40
41 final class DependenciesValidator implements MavenValidator {
42
43
44
45
46 private static final String SEP = String.format("%n\t");
47
48 @Override
49 @SuppressWarnings("PMD.OnlyOneReturn")
50 public void validate(final MavenEnvironment env)
51 throws ValidationException {
52 if (!env.outdir().exists() || "pom".equals(env.project().getPackaging())) {
53 Logger.info(this, "No dependency analysis in this project");
54 return;
55 }
56 final Collection<String> excludes = env.excludes("dependencies");
57 if (excludes.contains(".*")) {
58 Logger.info(this, "Dependency analysis suppressed in the project via pom.xml");
59 return;
60 }
61 final Collection<String> unused = Collections2.filter(
62 DependenciesValidator.unused(env),
63 Predicates.not(new DependenciesValidator.ExcludePredicate(excludes))
64 );
65 if (!unused.isEmpty()) {
66 Logger.warn(
67 this,
68 "Unused declared dependencies found:%s%s",
69 DependenciesValidator.SEP,
70 new Joined(DependenciesValidator.SEP, unused).toString()
71 );
72 }
73 final Collection<String> used = Collections2.filter(
74 DependenciesValidator.used(env),
75 Predicates.not(new DependenciesValidator.ExcludePredicate(excludes))
76 );
77 if (!used.isEmpty()) {
78 Logger.warn(
79 this,
80 "Used undeclared dependencies found:%s%s",
81 DependenciesValidator.SEP,
82 new Joined(DependenciesValidator.SEP, used)
83 );
84 }
85 if (!used.isEmpty() || !unused.isEmpty()) {
86 Logger.info(
87 this,
88 "You can suppress this message by <exclude>dependencies:...</exclude> in pom.xml, where <...> is what the dependency name starts with (not a regular expression!)"
89 );
90 }
91 final int failures = used.size() + unused.size();
92 if (failures > 0) {
93 throw new ValidationException(
94 String.format("%d dependency problem(s) found", failures)
95 );
96 }
97 Logger.info(this, "No dependency problems found");
98 }
99
100
101
102
103
104
105 private static ProjectDependencyAnalysis analyze(
106 final MavenEnvironment env) {
107 try {
108 return ((ProjectDependencyAnalyzer)
109 ((PlexusContainer)
110 env.context().get(PlexusConstants.PLEXUS_KEY)
111 ).lookup(ProjectDependencyAnalyzer.class.getName(), "default")
112 ).analyze(env.project());
113 } catch (final ContextException | ComponentLookupException
114 | ProjectDependencyAnalyzerException ex) {
115 throw new IllegalStateException(ex);
116 }
117 }
118
119
120
121
122
123
124 private static Collection<String> used(final MavenEnvironment env) {
125 final ProjectDependencyAnalysis analysis =
126 DependenciesValidator.analyze(env);
127 final Collection<String> used = new LinkedList<>();
128 for (final Object artifact : analysis.getUsedUndeclaredArtifacts()) {
129 used.add(artifact.toString());
130 }
131 return used;
132 }
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147 private static Collection<String> unused(final MavenEnvironment env) {
148 final ProjectDependencyAnalysis analysis =
149 DependenciesValidator.analyze(env);
150 final Set<String> imports = DependenciesValidator.imports(env);
151 final Collection<String> unused = new LinkedList<>();
152 for (final Object obj : analysis.getUnusedDeclaredArtifacts()) {
153 final Artifact artifact = (Artifact) obj;
154 if (!Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
155 continue;
156 }
157 if (DependenciesValidator.imported(imports, artifact)) {
158 Logger.info(
159 DependenciesValidator.class,
160 "Dependency %s is imported in source and treated as used (annotations or inlined constants are invisible to bytecode analysis)",
161 artifact
162 );
163 continue;
164 }
165 unused.add(artifact.toString());
166 }
167 return unused;
168 }
169
170
171
172
173
174
175
176 private static Set<String> imports(final MavenEnvironment env) {
177 final Set<String> imports = new HashSet<>();
178 final Collection<String> roots =
179 env.project().getCompileSourceRoots();
180 if (roots != null) {
181 for (final String root : roots) {
182 final Path dir = Paths.get(root);
183 if (Files.isDirectory(dir)) {
184 DependenciesValidator.scanJavaFiles(dir, imports);
185 }
186 }
187 }
188 return imports;
189 }
190
191
192
193
194
195
196 private static void scanJavaFiles(final Path dir, final Set<String> acc) {
197 try (Stream<Path> walk = Files.walk(dir)) {
198 walk
199 .filter(path -> path.toString().endsWith(".java"))
200 .forEach(path -> DependenciesValidator.readImports(path, acc));
201 } catch (final IOException ex) {
202 throw new IllegalStateException(
203 String.format("Cannot scan source root %s", dir), ex
204 );
205 }
206 }
207
208
209
210
211
212
213
214 private static void readImports(final Path file, final Set<String> acc) {
215 try {
216 for (final String line : Files.readAllLines(file, StandardCharsets.UTF_8)) {
217 final String trimmed = line.trim();
218 if (!trimmed.startsWith("import ")) {
219 continue;
220 }
221 final int semi = trimmed.indexOf(';');
222 if (semi < 0) {
223 continue;
224 }
225 String spec =
226 trimmed.substring("import ".length(), semi).trim();
227 if (spec.startsWith("static ")) {
228 spec = spec.substring("static ".length()).trim();
229 final int dot = spec.lastIndexOf('.');
230 if (dot > 0) {
231 spec = spec.substring(0, dot);
232 }
233 }
234 if (!spec.isEmpty()) {
235 acc.add(spec);
236 }
237 }
238 } catch (final IOException ex) {
239 throw new IllegalStateException(
240 String.format("Cannot read source file %s", file), ex
241 );
242 }
243 }
244
245
246
247
248
249
250
251 private static boolean imported(final Set<String> imports,
252 final Artifact artifact) {
253 final File file = artifact.getFile();
254 boolean found = false;
255 if (!imports.isEmpty() && file != null && file.isFile()) {
256 try (JarFile jar = new JarFile(file)) {
257 final Enumeration<JarEntry> entries = jar.entries();
258 while (!found && entries.hasMoreElements()) {
259 found = DependenciesValidator.matches(
260 imports, entries.nextElement().getName()
261 );
262 }
263 } catch (final IOException ex) {
264 Logger.warn(
265 DependenciesValidator.class,
266 "Cannot inspect %s while cross-checking imports: %s",
267 file, ex.getMessage()
268 );
269 }
270 }
271 return found;
272 }
273
274
275
276
277
278
279
280
281
282 private static boolean matches(final Set<String> imports,
283 final String entry) {
284 boolean match = false;
285 if (entry.endsWith(".class")
286 && !"module-info.class".equals(entry)
287 && entry.indexOf('$') < 0) {
288 final String fqn = entry
289 .substring(0, entry.length() - ".class".length())
290 .replace('/', '.');
291 final int dot = fqn.lastIndexOf('.');
292 match = imports.contains(fqn)
293 || dot > 0
294 && imports.contains(fqn.substring(0, dot).concat(".*"));
295 }
296 return match;
297 }
298
299
300
301
302
303 private static class ExcludePredicate implements Predicate<String> {
304
305
306
307
308 private final Collection<String> excludes;
309
310
311
312
313
314 ExcludePredicate(final Collection<String> excludes) {
315 this.excludes = excludes;
316 }
317
318 @Override
319 public boolean apply(final String name) {
320 boolean ignore = false;
321 for (final String exclude : this.excludes) {
322 if (name.startsWith(exclude)) {
323 ignore = true;
324 break;
325 }
326 }
327 return ignore;
328 }
329 }
330 }