View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.pmd.rules;
6   
7   import java.util.Collection;
8   import java.util.List;
9   import java.util.Set;
10  import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
11  import net.sourceforge.pmd.lang.java.ast.ASTClassDeclaration;
12  import net.sourceforge.pmd.lang.java.ast.ASTClassType;
13  import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
14  import net.sourceforge.pmd.lang.java.ast.ASTTypeDeclaration;
15  import net.sourceforge.pmd.lang.java.ast.ModifierOwner;
16  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
17  
18  /**
19   * Rule to check that a class does not expose too many methods. Unlike the
20   * PMD built-in {@code TooManyMethods} rule, this implementation counts
21   * only public and protected methods, because private and package-private
22   * ones are implementation detail and say nothing about how large the
23   * contract of the class is, and reports a class that exposes more than
24   * ten of them. Methods annotated with {@code @Override} are skipped too,
25   * since a supertype dictates them and the class has no say in how many
26   * of them there are. Test classes are skipped altogether, since
27   * one assertion per test method inflates their method count beyond any
28   * useful threshold. JNA bindings are skipped too: a type that extends
29   * {@code com.sun.jna.Library}, or its Windows flavour
30   * {@code com.sun.jna.win32.StdCallLibrary}, mirrors a native library
31   * method by method, and its size is dictated by that library rather than
32   * by its author.
33   * Skipping them here instead of through a
34   * {@code violationSuppressXPath} keeps a redundant
35   * {@code @SuppressWarnings("PMD.TooManyMethods")} visible to
36   * {@code UnnecessaryWarningSuppression}, which PMD credits to the
37   * annotation whenever the annotation suppressor runs first.
38   *
39   * @since 1.0
40   */
41  public final class TooManyMethodsRule extends AbstractJavaRulechainRule {
42  
43      /**
44       * The JNA interfaces that mark a type as a native binding.
45       */
46      private static final Collection<NamedSupertype> LIBRARIES = List.of(
47          new NamedSupertype("com.sun.jna.Library"),
48          new NamedSupertype("com.sun.jna.win32.StdCallLibrary")
49      );
50  
51      /**
52       * Simple-name suffixes that mark a type as a test.
53       */
54      private static final Set<String> SUFFIXES = Set.of(
55          "Test",
56          "Tests",
57          "IT",
58          "TestCase",
59          "ITCase"
60      );
61  
62      /**
63       * Annotations that mark a type as a test even when its name follows
64       * another convention. Names ending with {@code Test} are recognised
65       * separately, which covers {@code @Test}, {@code @ParameterizedTest},
66       * {@code @RepeatedTest} and their third-party equivalents.
67       */
68      private static final Set<String> ANNOTATIONS = Set.of(
69          "TestFactory",
70          "TestTemplate",
71          "Theory",
72          "Nested"
73      );
74  
75      /**
76       * Default constructor.
77       */
78      public TooManyMethodsRule() {
79          super(ASTClassDeclaration.class);
80      }
81  
82      @Override
83      public Object visit(final ASTClassDeclaration type, final Object data) {
84          if (!TooManyMethodsRule.tested(type)
85              && !TooManyMethodsRule.binding(type)
86              && TooManyMethodsRule.exposed(type) > 10) {
87              this.asCtx(data).addViolation(type);
88          }
89          return data;
90      }
91  
92      private static int exposed(final ASTClassDeclaration type) {
93          return type.getDeclarations(ASTMethodDeclaration.class)
94              .filter(TooManyMethodsRule::visible)
95              .count();
96      }
97  
98      private static boolean visible(final ASTMethodDeclaration method) {
99          return method.getVisibility()
100             .isAtLeast(ModifierOwner.Visibility.V_PROTECTED)
101             && !method.isAnnotationPresent(Override.class);
102     }
103 
104     private static boolean binding(final ASTClassDeclaration type) {
105         return type.getSuperInterfaceTypeNodes().any(TooManyMethodsRule::jna);
106     }
107 
108     private static boolean jna(final ASTClassType parent) {
109         return TooManyMethodsRule.LIBRARIES.stream().anyMatch(
110             known -> known.matches(parent)
111         );
112     }
113 
114     private static boolean tested(final ASTClassDeclaration type) {
115         return type.ancestorsOrSelf()
116             .filterIs(ASTTypeDeclaration.class)
117             .any(TooManyMethodsRule::marked);
118     }
119 
120     private static boolean marked(final ASTTypeDeclaration type) {
121         return TooManyMethodsRule.named(type.getSimpleName())
122             || type.descendants(ASTAnnotation.class).any(
123                 TooManyMethodsRule::testing
124             );
125     }
126 
127     private static boolean named(final String simple) {
128         return TooManyMethodsRule.SUFFIXES.stream().anyMatch(simple::endsWith);
129     }
130 
131     private static boolean testing(final ASTAnnotation annotation) {
132         return annotation.getSimpleName().endsWith("Test")
133             || TooManyMethodsRule.ANNOTATIONS.contains(
134                 annotation.getSimpleName()
135             );
136     }
137 }