View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
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   * Validator of dependencies.
37   *
38   * @since 0.3
39   * @checkstyle ReturnCountCheck (100 line)
40   */
41  final class DependenciesValidator implements MavenValidator {
42  
43      /**
44       * Separator between lines.
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 }