View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.dependency.analyze;
20  
21  import java.io.File;
22  import java.io.StringWriter;
23  import java.util.Arrays;
24  import java.util.Iterator;
25  import java.util.LinkedHashMap;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
33  import org.apache.maven.plugin.AbstractMojo;
34  import org.apache.maven.plugin.MojoExecutionException;
35  import org.apache.maven.plugin.MojoFailureException;
36  import org.apache.maven.plugins.annotations.Parameter;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
39  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
40  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzer;
41  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzerException;
42  import org.codehaus.plexus.PlexusContainer;
43  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
44  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
45  
46  /**
47   * Analyzes the dependencies of this project and determines which are: used and declared; used and undeclared; unused
48   * and declared; compile scoped but only used in tests.
49   *
50   * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
51   * @since 2.0-alpha-5
52   */
53  public abstract class AbstractAnalyzeMojo extends AbstractMojo {
54      // fields -----------------------------------------------------------------
55  
56      /**
57       * Specify the project dependency analyzer to use (plexus component role-hint). By default,
58       * <a href="/shared/maven-dependency-analyzer/">maven-dependency-analyzer</a> is used. To use this, you must declare
59       * a dependency for this plugin that contains the code for the analyzer. The analyzer must have a declared Plexus
60       * role name, and you specify the role name here.
61       *
62       * @since 2.2
63       */
64      @Parameter(property = "analyzer", defaultValue = "default")
65      private String analyzer;
66  
67      /**
68       * Whether to fail the build if a dependency warning is found.
69       */
70      @Parameter(property = "failOnWarning", defaultValue = "false")
71      private boolean failOnWarning;
72  
73      /**
74       * Output used dependencies.
75       */
76      @Parameter(property = "verbose", defaultValue = "false")
77      private boolean verbose;
78  
79      /**
80       * Ignore runtime/provided/test/system scopes for unused dependency analysis.
81       * <p>
82       * <code><b>Non-test scoped</b></code> list will be not affected.
83       */
84      @Parameter(property = "ignoreNonCompile", defaultValue = "false")
85      private boolean ignoreNonCompile;
86  
87      /**
88       * Ignore runtime scope for unused dependency analysis.
89       *
90       * @since 3.2.0
91       */
92      @Parameter(property = "ignoreUnusedRuntime", defaultValue = "false")
93      private boolean ignoreUnusedRuntime;
94  
95      /**
96       * Ignore all dependencies that are used only in test but not test-scoped. Setting
97       * this flag has the same effect as adding all dependencies that have been flagged with
98       * the <i>Non-test scoped test only dependencies found</i> warning to the
99       * <code>&lt;ignoredNonTestScopedDependencies&gt;</code> configuration.
100      *
101      * @since 3.3.1-SNAPSHOT
102      */
103     @Parameter(property = "ignoreAllNonTestScoped", defaultValue = "false")
104     private boolean ignoreAllNonTestScoped;
105 
106     /**
107      * Output the xml for the missing dependencies (used but not declared).
108      *
109      * @since 2.0-alpha-5
110      */
111     @Parameter(property = "outputXML", defaultValue = "false")
112     private boolean outputXML;
113 
114     /**
115      * Output scriptable values for the missing dependencies (used but not declared).
116      *
117      * @since 2.0-alpha-5
118      */
119     @Parameter(property = "scriptableOutput", defaultValue = "false")
120     private boolean scriptableOutput;
121 
122     /**
123      * Flag to use for scriptable output.
124      *
125      * @since 2.0-alpha-5
126      */
127     @Parameter(property = "scriptableFlag", defaultValue = "$$$%%%")
128     private String scriptableFlag;
129 
130     /**
131      * Flag to use for scriptable output.
132      *
133      * @since 2.0-alpha-5
134      */
135     @Parameter(defaultValue = "${basedir}", readonly = true)
136     private File baseDir;
137 
138     /**
139      * Target folder.
140      *
141      * @since 2.0-alpha-5
142      */
143     @Parameter(defaultValue = "${project.build.directory}", readonly = true)
144     private File outputDirectory;
145 
146     /**
147      * Force dependencies as used, to override incomplete result caused by bytecode-level analysis. Dependency format is
148      * <code>groupId:artifactId</code>.
149      *
150      * @since 2.6
151      */
152     @Parameter
153     private String[] usedDependencies;
154 
155     /**
156      * Skip plugin execution completely.
157      *
158      * @since 2.7
159      */
160     @Parameter(property = "mdep.analyze.skip", defaultValue = "false")
161     private boolean skip;
162 
163     /**
164      * List of dependencies that will be ignored. Any dependency on this list will be excluded from the "declared but
165      * unused", the "used but undeclared", and the "non-test scoped" list. The filter syntax is:
166      *
167      * <pre>
168      * [groupId]:[artifactId]:[type]:[version]
169      * </pre>
170      *
171      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
172      * segment is treated as an implicit wildcard. *
173      * <p>
174      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
175      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
176      * </p>
177      *
178      * @since 2.10
179      */
180     @Parameter
181     private String[] ignoredDependencies = new String[0];
182 
183     /**
184      * List of dependencies that will be ignored if they are used but undeclared. The filter syntax is:
185      *
186      * <pre>
187      * [groupId]:[artifactId]:[type]:[version]
188      * </pre>
189      *
190      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
191      * segment is treated as an implicit wildcard. *
192      * <p>
193      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
194      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
195      * </p>
196      *
197      * @since 2.10
198      */
199     @Parameter
200     private String[] ignoredUsedUndeclaredDependencies = new String[0];
201 
202     /**
203      * List of dependencies that are ignored if they are declared but unused. The filter syntax is:
204      *
205      * <pre>
206      * [groupId]:[artifactId]:[type]:[version]
207      * </pre>
208      *
209      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
210      * segment is treated as an implicit wildcard. *
211      * <p>
212      * For example, <code>org.apache.*</code> matches all artifacts whose group id starts with
213      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> matches all snapshot artifacts.
214      * </p>
215      *
216      * <p>Certain dependencies that are known to be used and loaded by reflection
217      * are always ignored. This includes {@code org.slf4j:slf4j-simple::}.</p>
218      *
219      * @since 2.10
220      */
221     @Parameter
222     private String[] ignoredUnusedDeclaredDependencies = new String[0];
223 
224     private String[] unconditionallyIgnoredDeclaredDependencies = {"org.slf4j:slf4j-simple::"};
225 
226     /**
227      * List of dependencies that are ignored if they are in not test scope but are only used in test classes.
228      * The filter syntax is:
229      *
230      * <pre>
231      * [groupId]:[artifactId]:[type]:[version]
232      * </pre>
233      *
234      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
235      * segment is treated as an implicit wildcard. *
236      * <p>
237      * For example, <code>org.apache.*</code> matched all artifacts whose group id starts with
238      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
239      * </p>
240      *
241      * @since 3.3.0
242      */
243     @Parameter(defaultValue = "org.slf4j:slf4j-simple::")
244     private String[] ignoredNonTestScopedDependencies;
245 
246     /**
247      * List of project packaging that will be ignored.
248      * <br/>
249      * <b>Default value is<b>: <code>pom, ear</code>
250      *
251      * @since 3.2.1
252      */
253     @Parameter(defaultValue = "pom,ear")
254     private List<String> ignoredPackagings;
255 
256     /**
257      * List of class patterns excluded from analyze. Java regular expression pattern is applied to full class name.
258      *
259      * @since 3.7.0
260      */
261     @Parameter(property = "mdep.analyze.excludedClasses")
262     private Set<String> excludedClasses;
263 
264     /**
265      * The plexusContainer to look up the {@link ProjectDependencyAnalyzer} implementation depending on the mojo
266      * configuration.
267      */
268     private final PlexusContainer plexusContainer;
269 
270     /**
271      * The Maven project to analyze.
272      */
273     private final MavenProject project;
274 
275     protected AbstractAnalyzeMojo(PlexusContainer plexusContainer, MavenProject project) {
276         this.plexusContainer = plexusContainer;
277         this.project = project;
278     }
279 
280     // Mojo methods -----------------------------------------------------------
281 
282     /*
283      * @see org.apache.maven.plugin.Mojo#execute()
284      */
285     @Override
286     public void execute() throws MojoExecutionException, MojoFailureException {
287         if (isSkip()) {
288             getLog().info("Skipping plugin execution");
289             return;
290         }
291 
292         if (ignoredPackagings.contains(project.getPackaging())) {
293             getLog().info("Skipping " + project.getPackaging() + " project");
294             return;
295         }
296 
297         if (outputDirectory == null || !outputDirectory.exists()) {
298             getLog().info("Skipping project with no build directory");
299             return;
300         }
301 
302         boolean warning = checkDependencies();
303 
304         if (warning && failOnWarning) {
305             throw new MojoExecutionException("Dependency problems found");
306         }
307     }
308 
309     /**
310      * @return {@link ProjectDependencyAnalyzer}
311      * @throws MojoExecutionException in case of an error
312      */
313     protected ProjectDependencyAnalyzer createProjectDependencyAnalyzer() throws MojoExecutionException {
314 
315         try {
316             return plexusContainer.lookup(ProjectDependencyAnalyzer.class, analyzer);
317         } catch (ComponentLookupException exception) {
318             throw new MojoExecutionException(
319                     "Failed to instantiate ProjectDependencyAnalyser" + " / role-hint " + analyzer, exception);
320         }
321     }
322 
323     /**
324      * @return {@link #skip}
325      */
326     protected final boolean isSkip() {
327         return skip;
328     }
329 
330     // private methods --------------------------------------------------------
331 
332     private boolean checkDependencies() throws MojoExecutionException {
333         ProjectDependencyAnalysis analysis;
334         try {
335             analysis = createProjectDependencyAnalyzer().analyze(project, excludedClasses);
336 
337             if (usedDependencies != null) {
338                 analysis = analysis.forceDeclaredDependenciesUsage(usedDependencies);
339             }
340         } catch (ProjectDependencyAnalyzerException exception) {
341             throw new MojoExecutionException("Cannot analyze dependencies", exception);
342         }
343 
344         if (ignoreNonCompile) {
345             analysis = analysis.ignoreNonCompile();
346         }
347 
348         Set<Artifact> usedDeclared = new LinkedHashSet<>(analysis.getUsedDeclaredArtifacts());
349         Map<Artifact, Set<String>> usedUndeclaredWithClasses =
350                 new LinkedHashMap<>(analysis.getUsedUndeclaredArtifactsWithClasses());
351         Set<Artifact> unusedDeclared = new LinkedHashSet<>(analysis.getUnusedDeclaredArtifacts());
352         Set<Artifact> nonTestScope = new LinkedHashSet<>(analysis.getTestArtifactsWithNonTestScope());
353 
354         Set<Artifact> ignoredUsedUndeclared = new LinkedHashSet<>();
355         Set<Artifact> ignoredUnusedDeclared = new LinkedHashSet<>();
356         Set<Artifact> ignoredNonTestScope = new LinkedHashSet<>();
357 
358         if (ignoreUnusedRuntime) {
359             filterArtifactsByScope(unusedDeclared, Artifact.SCOPE_RUNTIME);
360         }
361 
362         ignoredUsedUndeclared.addAll(filterDependencies(usedUndeclaredWithClasses.keySet(), ignoredDependencies));
363         ignoredUsedUndeclared.addAll(
364                 filterDependencies(usedUndeclaredWithClasses.keySet(), ignoredUsedUndeclaredDependencies));
365 
366         ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredDependencies));
367         ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredUnusedDeclaredDependencies));
368         ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, unconditionallyIgnoredDeclaredDependencies));
369 
370         if (ignoreAllNonTestScoped) {
371             ignoredNonTestScope.addAll(filterDependencies(nonTestScope, new String[] {"*"}));
372         } else {
373             ignoredNonTestScope.addAll(filterDependencies(nonTestScope, ignoredDependencies));
374             ignoredNonTestScope.addAll(filterDependencies(nonTestScope, ignoredNonTestScopedDependencies));
375         }
376 
377         boolean reported = false;
378         boolean warning = false;
379 
380         if (verbose && !usedDeclared.isEmpty()) {
381             getLog().info("Used declared dependencies found:");
382 
383             logArtifacts(analysis.getUsedDeclaredArtifacts(), false);
384             reported = true;
385         }
386 
387         if (!usedUndeclaredWithClasses.isEmpty()) {
388             logDependencyWarning("Used undeclared dependencies found:");
389 
390             if (verbose) {
391                 logArtifacts(usedUndeclaredWithClasses);
392             } else {
393                 logArtifacts(usedUndeclaredWithClasses.keySet(), true);
394             }
395             reported = true;
396             warning = true;
397         }
398 
399         if (!unusedDeclared.isEmpty()) {
400             logDependencyWarning("Unused declared dependencies found:");
401 
402             logArtifacts(unusedDeclared, true);
403             reported = true;
404             warning = true;
405         }
406 
407         if (!nonTestScope.isEmpty()) {
408             logDependencyWarning("Non-test scoped test only dependencies found:");
409 
410             logArtifacts(nonTestScope, true);
411             reported = true;
412             warning = true;
413         }
414 
415         if (verbose && !ignoredUsedUndeclared.isEmpty()) {
416             getLog().info("Ignored used undeclared dependencies:");
417 
418             logArtifacts(ignoredUsedUndeclared, false);
419             reported = true;
420         }
421 
422         if (verbose && !ignoredUnusedDeclared.isEmpty()) {
423             getLog().info("Ignored unused declared dependencies:");
424 
425             logArtifacts(ignoredUnusedDeclared, false);
426             reported = true;
427         }
428 
429         if (verbose && !ignoredNonTestScope.isEmpty()) {
430             getLog().info("Ignored non-test scoped test only dependencies:");
431 
432             logArtifacts(ignoredNonTestScope, false);
433             reported = true;
434         }
435 
436         if (outputXML) {
437             writeDependencyXML(usedUndeclaredWithClasses.keySet());
438         }
439 
440         if (scriptableOutput) {
441             writeScriptableOutput(usedUndeclaredWithClasses.keySet());
442         }
443 
444         if (!reported) {
445             getLog().info("No dependency problems found");
446         }
447 
448         return warning;
449     }
450 
451     private void filterArtifactsByScope(Set<Artifact> artifacts, String scope) {
452         artifacts.removeIf(artifact -> artifact.getScope().equals(scope));
453     }
454 
455     private void logArtifacts(Set<Artifact> artifacts, boolean warn) {
456         if (artifacts.isEmpty()) {
457             getLog().info("   None");
458         } else {
459             for (Artifact artifact : artifacts) {
460                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
461                 artifact.isSnapshot();
462 
463                 if (warn) {
464                     logDependencyWarning("   " + artifact);
465                 } else {
466                     getLog().info("   " + artifact);
467                 }
468             }
469         }
470     }
471 
472     private void logArtifacts(Map<Artifact, Set<String>> artifacts) {
473         if (artifacts.isEmpty()) {
474             getLog().info("   None");
475         } else {
476             for (Map.Entry<Artifact, Set<String>> entry : artifacts.entrySet()) {
477                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
478                 entry.getKey().isSnapshot();
479 
480                 logDependencyWarning("   " + entry.getKey());
481                 for (String clazz : entry.getValue()) {
482                     logDependencyWarning("      class " + clazz);
483                 }
484             }
485         }
486     }
487 
488     private void logDependencyWarning(CharSequence content) {
489         if (failOnWarning) {
490             getLog().error(content);
491         } else {
492             getLog().warn(content);
493         }
494     }
495 
496     private void writeDependencyXML(Set<Artifact> artifacts) {
497         if (!artifacts.isEmpty()) {
498             getLog().info("Add the following to your pom to correct the missing dependencies: ");
499 
500             StringWriter out = new StringWriter();
501             PrettyPrintXMLWriter writer = new PrettyPrintXMLWriter(out);
502 
503             for (Artifact artifact : artifacts) {
504                 writer.startElement("dependency");
505                 writer.startElement("groupId");
506                 writer.writeText(artifact.getGroupId());
507                 writer.endElement();
508                 writer.startElement("artifactId");
509                 writer.writeText(artifact.getArtifactId());
510                 writer.endElement();
511                 writer.startElement("version");
512                 writer.writeText(artifact.getBaseVersion());
513                 String classifier = artifact.getClassifier();
514                 if (classifier != null && !classifier.trim().isEmpty()) {
515                     writer.startElement("classifier");
516                     writer.writeText(artifact.getClassifier());
517                     writer.endElement();
518                 }
519                 writer.endElement();
520 
521                 if (!Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
522                     writer.startElement("scope");
523                     writer.writeText(artifact.getScope());
524                     writer.endElement();
525                 }
526                 writer.endElement();
527             }
528 
529             getLog().info(System.lineSeparator() + out.getBuffer());
530         }
531     }
532 
533     private void writeScriptableOutput(Set<Artifact> artifacts) {
534         if (!artifacts.isEmpty()) {
535             getLog().info("Missing dependencies: ");
536             String pomFile = baseDir.getAbsolutePath() + File.separatorChar + "pom.xml";
537             StringBuilder buf = new StringBuilder();
538 
539             for (Artifact artifact : artifacts) {
540                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
541                 artifact.isSnapshot();
542 
543                 buf.append(scriptableFlag)
544                         .append(":")
545                         .append(pomFile)
546                         .append(":")
547                         .append(artifact.getDependencyConflictId())
548                         .append(":")
549                         .append(artifact.getClassifier())
550                         .append(":")
551                         .append(artifact.getBaseVersion())
552                         .append(":")
553                         .append(artifact.getScope())
554                         .append(System.lineSeparator());
555             }
556             getLog().info(System.lineSeparator() + buf);
557         }
558     }
559 
560     private Set<Artifact> filterDependencies(Set<Artifact> artifacts, String[] excludes) {
561         if (excludes == null || excludes.length == 0) {
562             return artifacts;
563         }
564         ArtifactFilter filter = new StrictPatternExcludesArtifactFilter(Arrays.asList(excludes));
565         Set<Artifact> result = new LinkedHashSet<>();
566 
567         for (Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); ) {
568             Artifact artifact = it.next();
569             if (!filter.include(artifact)) {
570                 it.remove();
571                 result.add(artifact);
572             }
573         }
574 
575         return result;
576     }
577 }