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