1
2
3
4
5 package com.qulice.maven;
6
7 import com.google.common.base.Predicates;
8 import com.google.common.collect.Collections2;
9 import com.jcabi.log.Logger;
10 import com.qulice.spi.ValidationException;
11 import java.io.File;
12 import java.io.IOException;
13 import java.nio.charset.StandardCharsets;
14 import java.nio.file.Files;
15 import java.nio.file.Path;
16 import java.nio.file.Paths;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Enumeration;
20 import java.util.HashSet;
21 import java.util.Set;
22 import java.util.jar.JarEntry;
23 import java.util.jar.JarFile;
24 import java.util.stream.Stream;
25 import org.apache.maven.artifact.Artifact;
26 import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
27 import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzer;
28 import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzerException;
29 import org.cactoos.text.Joined;
30 import org.codehaus.plexus.PlexusConstants;
31 import org.codehaus.plexus.PlexusContainer;
32 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
33 import org.codehaus.plexus.context.ContextException;
34
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 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 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 private static ProjectDependencyAnalysis analyze(
101 final MavenEnvironment env) {
102 try {
103 return ((ProjectDependencyAnalyzer)
104 ((PlexusContainer)
105 env.context().get(PlexusConstants.PLEXUS_KEY)
106 ).lookup(ProjectDependencyAnalyzer.class.getName(), "default")
107 ).analyze(env.project());
108 } catch (final ContextException | ComponentLookupException
109 | ProjectDependencyAnalyzerException ex) {
110 throw new IllegalStateException(ex);
111 }
112 }
113
114 private static Collection<String> used(final MavenEnvironment env) {
115 final ProjectDependencyAnalysis analysis =
116 DependenciesValidator.analyze(env);
117 final Collection<String> used = new ArrayList<>(0);
118 for (final Object artifact : analysis.getUsedUndeclaredArtifacts()) {
119 used.add(artifact.toString());
120 }
121 return used;
122 }
123
124 private static Collection<String> unused(final MavenEnvironment env) {
125 final ProjectDependencyAnalysis analysis =
126 DependenciesValidator.analyze(env);
127 final Set<String> imports = DependenciesValidator.imports(env);
128 final Collection<String> unused = new ArrayList<>(0);
129 for (final Object obj : analysis.getUnusedDeclaredArtifacts()) {
130 final Artifact artifact = (Artifact) obj;
131 if (!Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
132 continue;
133 }
134 if (DependenciesValidator.imported(imports, artifact)) {
135 Logger.info(
136 DependenciesValidator.class,
137 "Dependency %s is imported in source and treated as used (annotations or inlined constants are invisible to bytecode analysis)",
138 artifact
139 );
140 continue;
141 }
142 unused.add(artifact.toString());
143 }
144 return unused;
145 }
146
147 private static Set<String> imports(final MavenEnvironment env) {
148 final Set<String> imports = new HashSet<>();
149 final Collection<String> roots =
150 env.project().getCompileSourceRoots();
151 if (roots != null) {
152 for (final String root : roots) {
153 final Path dir = Paths.get(root);
154 if (Files.isDirectory(dir)) {
155 DependenciesValidator.scanJavaFiles(dir, imports);
156 }
157 }
158 }
159 return imports;
160 }
161
162 private static void scanJavaFiles(final Path dir, final Set<String> acc) {
163 try (Stream<Path> walk = Files.walk(dir)) {
164 walk
165 .filter(path -> path.toString().endsWith(".java"))
166 .forEach(path -> DependenciesValidator.readImports(path, acc));
167 } catch (final IOException ex) {
168 throw new IllegalStateException(
169 String.format("Cannot scan source root %s", dir), ex
170 );
171 }
172 }
173
174 private static void readImports(final Path file, final Set<String> acc) {
175 try {
176 for (final String line : Files.readAllLines(file, StandardCharsets.UTF_8)) {
177 final String trimmed = line.trim();
178 if (!trimmed.startsWith("import ")) {
179 continue;
180 }
181 final int semi = trimmed.indexOf(';');
182 if (semi < 0) {
183 continue;
184 }
185 String spec =
186 trimmed.substring("import ".length(), semi).trim();
187 if (spec.startsWith("static ")) {
188 spec = spec.substring("static ".length()).trim();
189 final int dot = spec.lastIndexOf('.');
190 if (dot > 0) {
191 spec = spec.substring(0, dot);
192 }
193 }
194 if (!spec.isEmpty()) {
195 acc.add(spec);
196 }
197 }
198 } catch (final IOException ex) {
199 throw new IllegalStateException(
200 String.format("Cannot read source file %s", file), ex
201 );
202 }
203 }
204
205 private static boolean imported(final Set<String> imports,
206 final Artifact artifact) {
207 final File file = artifact.getFile();
208 boolean found = false;
209 if (!imports.isEmpty() && file != null && file.isFile()) {
210 try (JarFile jar = new JarFile(file)) {
211 final Enumeration<JarEntry> entries = jar.entries();
212 while (!found && entries.hasMoreElements()) {
213 found = DependenciesValidator.matches(
214 imports, entries.nextElement().getName()
215 );
216 }
217 } catch (final IOException ex) {
218 Logger.warn(
219 DependenciesValidator.class,
220 "Cannot inspect %s while cross-checking imports: %s",
221 file, ex.getMessage()
222 );
223 }
224 }
225 return found;
226 }
227
228 private static boolean matches(final Set<String> imports,
229 final String entry) {
230 boolean match = false;
231 if (entry.endsWith(".class")
232 && !"module-info.class".equals(entry)
233 && entry.indexOf('$') < 0) {
234 final String fqn = entry
235 .substring(0, entry.length() - ".class".length())
236 .replace('/', '.');
237 final int dot = fqn.lastIndexOf('.');
238 final boolean direct = imports.contains(fqn);
239 final boolean wildcard = dot > 0
240 && imports.contains(fqn.substring(0, dot).concat(".*"));
241 match = direct || wildcard;
242 }
243 return match;
244 }
245 }