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