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.maven.artifact.Artifact;
33  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugins.annotations.Component;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.plugins.dependency.utils.StringUtils;
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      @Component
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       * <p>
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     /**
270      * List Excluded classes patterns from analyze. Java regular expression pattern is applied to full class name.
271      *
272      * @since 3.7.0
273      */
274     @Parameter(property = "mdep.analyze.excludedClasses")
275     private Set<String> excludedClasses;
276 
277     // Mojo methods -----------------------------------------------------------
278 
279     /*
280      * @see org.apache.maven.plugin.Mojo#execute()
281      */
282     @Override
283     public void execute() throws MojoExecutionException, MojoFailureException {
284         if (isSkip()) {
285             getLog().info("Skipping plugin execution");
286             return;
287         }
288 
289         if (ignoredPackagings.contains(project.getPackaging())) {
290             getLog().info("Skipping " + project.getPackaging() + " project");
291             return;
292         }
293 
294         if (outputDirectory == null || !outputDirectory.exists()) {
295             getLog().info("Skipping project with no build directory");
296             return;
297         }
298 
299         boolean warning = checkDependencies();
300 
301         if (warning && failOnWarning) {
302             throw new MojoExecutionException("Dependency problems found");
303         }
304     }
305 
306     /**
307      * @return {@link ProjectDependencyAnalyzer}
308      * @throws MojoExecutionException in case of an error.
309      */
310     protected ProjectDependencyAnalyzer createProjectDependencyAnalyzer() throws MojoExecutionException {
311 
312         try {
313             return plexusContainer.lookup(ProjectDependencyAnalyzer.class, analyzer);
314         } catch (ComponentLookupException exception) {
315             throw new MojoExecutionException(
316                     "Failed to instantiate ProjectDependencyAnalyser" + " / role-hint " + analyzer, exception);
317         }
318     }
319 
320     /**
321      * @return {@link #skip}
322      */
323     protected final boolean isSkip() {
324         return skip;
325     }
326 
327     // private methods --------------------------------------------------------
328 
329     private boolean checkDependencies() throws MojoExecutionException {
330         ProjectDependencyAnalysis analysis;
331         try {
332             analysis = createProjectDependencyAnalyzer().analyze(project, excludedClasses);
333 
334             if (usedDependencies != null) {
335                 analysis = analysis.forceDeclaredDependenciesUsage(usedDependencies);
336             }
337         } catch (ProjectDependencyAnalyzerException exception) {
338             throw new MojoExecutionException("Cannot analyze dependencies", exception);
339         }
340 
341         if (ignoreNonCompile) {
342             analysis = analysis.ignoreNonCompile();
343         }
344 
345         Set<Artifact> usedDeclared = new LinkedHashSet<>(analysis.getUsedDeclaredArtifacts());
346         Map<Artifact, Set<String>> usedUndeclaredWithClasses =
347                 new LinkedHashMap<>(analysis.getUsedUndeclaredArtifactsWithClasses());
348         Set<Artifact> unusedDeclared = new LinkedHashSet<>(analysis.getUnusedDeclaredArtifacts());
349         Set<Artifact> nonTestScope = new LinkedHashSet<>(analysis.getTestArtifactsWithNonTestScope());
350 
351         Set<Artifact> ignoredUsedUndeclared = new LinkedHashSet<>();
352         Set<Artifact> ignoredUnusedDeclared = new LinkedHashSet<>();
353         Set<Artifact> ignoredNonTestScope = new LinkedHashSet<>();
354 
355         if (ignoreUnusedRuntime) {
356             filterArtifactsByScope(unusedDeclared, Artifact.SCOPE_RUNTIME);
357         }
358 
359         ignoredUsedUndeclared.addAll(filterDependencies(usedUndeclaredWithClasses.keySet(), ignoredDependencies));
360         ignoredUsedUndeclared.addAll(
361                 filterDependencies(usedUndeclaredWithClasses.keySet(), ignoredUsedUndeclaredDependencies));
362 
363         ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredDependencies));
364         ignoredUnusedDeclared.addAll(filterDependencies(unusedDeclared, ignoredUnusedDeclaredDependencies));
365 
366         if (ignoreAllNonTestScoped) {
367             ignoredNonTestScope.addAll(filterDependencies(nonTestScope, new String[] {"*"}));
368         } else {
369             ignoredNonTestScope.addAll(filterDependencies(nonTestScope, ignoredDependencies));
370             ignoredNonTestScope.addAll(filterDependencies(nonTestScope, ignoredNonTestScopedDependencies));
371         }
372 
373         boolean reported = false;
374         boolean warning = false;
375 
376         if (verbose && !usedDeclared.isEmpty()) {
377             getLog().info("Used declared dependencies found:");
378 
379             logArtifacts(analysis.getUsedDeclaredArtifacts(), false);
380             reported = true;
381         }
382 
383         if (!usedUndeclaredWithClasses.isEmpty()) {
384             logDependencyWarning("Used undeclared dependencies found:");
385 
386             if (verbose) {
387                 logArtifacts(usedUndeclaredWithClasses, true);
388             } else {
389                 logArtifacts(usedUndeclaredWithClasses.keySet(), true);
390             }
391             reported = true;
392             warning = true;
393         }
394 
395         if (!unusedDeclared.isEmpty()) {
396             logDependencyWarning("Unused declared dependencies found:");
397 
398             logArtifacts(unusedDeclared, true);
399             reported = true;
400             warning = true;
401         }
402 
403         if (!nonTestScope.isEmpty()) {
404             logDependencyWarning("Non-test scoped test only dependencies found:");
405 
406             logArtifacts(nonTestScope, true);
407             reported = true;
408             warning = true;
409         }
410 
411         if (verbose && !ignoredUsedUndeclared.isEmpty()) {
412             getLog().info("Ignored used undeclared dependencies:");
413 
414             logArtifacts(ignoredUsedUndeclared, false);
415             reported = true;
416         }
417 
418         if (verbose && !ignoredUnusedDeclared.isEmpty()) {
419             getLog().info("Ignored unused declared dependencies:");
420 
421             logArtifacts(ignoredUnusedDeclared, false);
422             reported = true;
423         }
424 
425         if (verbose && !ignoredNonTestScope.isEmpty()) {
426             getLog().info("Ignored non-test scoped test only dependencies:");
427 
428             logArtifacts(ignoredNonTestScope, false);
429             reported = true;
430         }
431 
432         if (outputXML) {
433             writeDependencyXML(usedUndeclaredWithClasses.keySet());
434         }
435 
436         if (scriptableOutput) {
437             writeScriptableOutput(usedUndeclaredWithClasses.keySet());
438         }
439 
440         if (!reported) {
441             getLog().info("No dependency problems found");
442         }
443 
444         return warning;
445     }
446 
447     private void filterArtifactsByScope(Set<Artifact> artifacts, String scope) {
448         artifacts.removeIf(artifact -> artifact.getScope().equals(scope));
449     }
450 
451     private void logArtifacts(Set<Artifact> artifacts, boolean warn) {
452         if (artifacts.isEmpty()) {
453             getLog().info("   None");
454         } else {
455             for (Artifact artifact : artifacts) {
456                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
457                 artifact.isSnapshot();
458 
459                 if (warn) {
460                     logDependencyWarning("   " + artifact);
461                 } else {
462                     getLog().info("   " + artifact);
463                 }
464             }
465         }
466     }
467 
468     private void logArtifacts(Map<Artifact, Set<String>> artifacts, boolean warn) {
469         if (artifacts.isEmpty()) {
470             getLog().info("   None");
471         } else {
472             for (Map.Entry<Artifact, Set<String>> entry : artifacts.entrySet()) {
473                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
474                 entry.getKey().isSnapshot();
475 
476                 if (warn) {
477                     logDependencyWarning("   " + entry.getKey());
478                     for (String clazz : entry.getValue()) {
479                         logDependencyWarning("      class " + clazz);
480                     }
481                 } else {
482                     getLog().info("   " + entry.getKey());
483                     for (String clazz : entry.getValue()) {
484                         getLog().info("      class " + clazz);
485                     }
486                 }
487             }
488         }
489     }
490 
491     private void logDependencyWarning(CharSequence content) {
492         if (failOnWarning) {
493             getLog().error(content);
494         } else {
495             getLog().warn(content);
496         }
497     }
498 
499     private void writeDependencyXML(Set<Artifact> artifacts) {
500         if (!artifacts.isEmpty()) {
501             getLog().info("Add the following to your pom to correct the missing dependencies: ");
502 
503             StringWriter out = new StringWriter();
504             PrettyPrintXMLWriter writer = new PrettyPrintXMLWriter(out);
505 
506             for (Artifact artifact : artifacts) {
507                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
508                 artifact.isSnapshot();
509 
510                 writer.startElement("dependency");
511                 writer.startElement("groupId");
512                 writer.writeText(artifact.getGroupId());
513                 writer.endElement();
514                 writer.startElement("artifactId");
515                 writer.writeText(artifact.getArtifactId());
516                 writer.endElement();
517                 writer.startElement("version");
518                 writer.writeText(artifact.getBaseVersion());
519                 String classifier = artifact.getClassifier();
520                 if (!StringUtils.isEmpty(classifier)) {
521                     writer.startElement("classifier");
522                     writer.writeText(classifier);
523                     writer.endElement();
524                 }
525                 writer.endElement();
526 
527                 if (!Artifact.SCOPE_COMPILE.equals(artifact.getScope())) {
528                     writer.startElement("scope");
529                     writer.writeText(artifact.getScope());
530                     writer.endElement();
531                 }
532                 writer.endElement();
533             }
534 
535             getLog().info(System.lineSeparator() + out.getBuffer());
536         }
537     }
538 
539     private void writeScriptableOutput(Set<Artifact> artifacts) {
540         if (!artifacts.isEmpty()) {
541             getLog().info("Missing dependencies: ");
542             String pomFile = baseDir.getAbsolutePath() + File.separatorChar + "pom.xml";
543             StringBuilder buf = new StringBuilder();
544 
545             for (Artifact artifact : artifacts) {
546                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
547                 artifact.isSnapshot();
548 
549                 buf.append(scriptableFlag)
550                         .append(":")
551                         .append(pomFile)
552                         .append(":")
553                         .append(artifact.getDependencyConflictId())
554                         .append(":")
555                         .append(artifact.getClassifier())
556                         .append(":")
557                         .append(artifact.getBaseVersion())
558                         .append(":")
559                         .append(artifact.getScope())
560                         .append(System.lineSeparator());
561             }
562             getLog().info(System.lineSeparator() + buf);
563         }
564     }
565 
566     private List<Artifact> filterDependencies(Set<Artifact> artifacts, String[] excludes) {
567         ArtifactFilter filter = new StrictPatternExcludesArtifactFilter(Arrays.asList(excludes));
568         List<Artifact> result = new ArrayList<>();
569 
570         for (Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); ) {
571             Artifact artifact = it.next();
572             if (!filter.include(artifact)) {
573                 it.remove();
574                 result.add(artifact);
575             }
576         }
577 
578         return result;
579     }
580 }