View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.design;
5   
6   import java.util.ArrayList;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.ListIterator;
10  
11  import net.sourceforge.pmd.lang.java.ast.ASTAllocationExpression;
12  import net.sourceforge.pmd.lang.java.ast.ASTArguments;
13  import net.sourceforge.pmd.lang.java.ast.ASTArrayDimsAndInits;
14  import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
15  import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
16  import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
17  import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration;
18  import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration;
19  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
20  import net.sourceforge.pmd.lang.java.symboltable.SourceFileScope;
21  
22  /**
23   * 1. Note all private constructors. 2. Note all instantiations from outside of
24   * the class by way of the private constructor. 3. Flag instantiations.
25   * <p/>
26   * <p/>
27   * Parameter types can not be matched because they can come as exposed members
28   * of classes. In this case we have no way to know what the type is. We can make
29   * a best effort though which can filter some?
30   *
31   * @author CL Gilbert (dnoyeb@users.sourceforge.net)
32   * @author David Konecny (david.konecny@)
33   * @author Romain PELISSE, belaran@gmail.com, patch bug#1807370
34   */
35  public class AccessorClassGenerationRule extends AbstractJavaRule {
36  
37      private List<ClassData> classDataList = new ArrayList<>();
38      private int classID = -1;
39      private String packageName;
40  
41      public Object visit(ASTEnumDeclaration node, Object data) {
42          return data; // just skip Enums
43      }
44  
45      public Object visit(ASTCompilationUnit node, Object data) {
46          classDataList.clear();
47          packageName = node.getScope().getEnclosingScope(SourceFileScope.class).getPackageName();
48          return super.visit(node, data);
49      }
50  
51      private static class ClassData {
52          private String className;
53          private List<ASTConstructorDeclaration> privateConstructors;
54          private List<AllocData> instantiations;
55          /**
56           * List of outer class names that exist above this class
57           */
58          private List<String> classQualifyingNames;
59  
60          public ClassData(String className) {
61              this.className = className;
62              this.privateConstructors = new ArrayList<>();
63              this.instantiations = new ArrayList<>();
64              this.classQualifyingNames = new ArrayList<>();
65          }
66  
67          public void addInstantiation(AllocData ad) {
68              instantiations.add(ad);
69          }
70  
71          public Iterator<AllocData> getInstantiationIterator() {
72              return instantiations.iterator();
73          }
74  
75          public void addConstructor(ASTConstructorDeclaration cd) {
76              privateConstructors.add(cd);
77          }
78  
79          public Iterator<ASTConstructorDeclaration> getPrivateConstructorIterator() {
80              return privateConstructors.iterator();
81          }
82  
83          public String getClassName() {
84              return className;
85          }
86  
87          public void addClassQualifyingName(String name) {
88              classQualifyingNames.add(name);
89          }
90  
91          public List<String> getClassQualifyingNamesList() {
92              return classQualifyingNames;
93          }
94      }
95  
96      private static class AllocData {
97          private String name;
98          private int argumentCount;
99          private ASTAllocationExpression allocationExpression;
100         private boolean isArray;
101 
102         public AllocData(ASTAllocationExpression node, String aPackageName, List<String> classQualifyingNames) {
103             if (node.jjtGetChild(1) instanceof ASTArguments) {
104                 ASTArguments aa = (ASTArguments) node.jjtGetChild(1);
105                 argumentCount = aa.getArgumentCount();
106                 // Get name and strip off all superfluous data
107                 // strip off package name if it is current package
108                 if (!(node.jjtGetChild(0) instanceof ASTClassOrInterfaceType)) {
109                     throw new RuntimeException("BUG: Expected a ASTClassOrInterfaceType, got a "
110                             + node.jjtGetChild(0).getClass());
111                 }
112                 ASTClassOrInterfaceType an = (ASTClassOrInterfaceType) node.jjtGetChild(0);
113                 name = stripString(aPackageName + '.', an.getImage());
114 
115                 // strip off outer class names
116                 // try OuterClass, then try OuterClass.InnerClass, then try
117                 // OuterClass.InnerClass.InnerClass2, etc...
118                 String findName = "";
119                 for (ListIterator<String> li = classQualifyingNames.listIterator(classQualifyingNames.size()); li
120                         .hasPrevious();) {
121                     String aName = li.previous();
122                     findName = aName + '.' + findName;
123                     if (name.startsWith(findName)) {
124                         // strip off name and exit
125                         name = name.substring(findName.length());
126                         break;
127                     }
128                 }
129             } else if (node.jjtGetChild(1) instanceof ASTArrayDimsAndInits) {
130                 // this is incomplete because I dont need it.
131                 // child 0 could be primitive or object (ASTName or
132                 // ASTPrimitiveType)
133                 isArray = true;
134             }
135             allocationExpression = node;
136         }
137 
138         public String getName() {
139             return name;
140         }
141 
142         public int getArgumentCount() {
143             return argumentCount;
144         }
145 
146         public ASTAllocationExpression getASTAllocationExpression() {
147             return allocationExpression;
148         }
149 
150         public boolean isArray() {
151             return isArray;
152         }
153     }
154 
155     /**
156      * Outer interface visitation
157      */
158     public Object visit(ASTClassOrInterfaceDeclaration node, Object data) {
159         if (node.isInterface()) {
160             if (!(node.jjtGetParent().jjtGetParent() instanceof ASTCompilationUnit)) {
161                 // not a top level interface
162                 String interfaceName = node.getImage();
163                 int formerID = getClassID();
164                 setClassID(classDataList.size());
165                 ClassData newClassData = new ClassData(interfaceName);
166                 // store the names of any outer classes of this class in the
167                 // classQualifyingName List
168                 ClassData formerClassData = classDataList.get(formerID);
169                 newClassData.addClassQualifyingName(formerClassData.getClassName());
170                 classDataList.add(getClassID(), newClassData);
171                 Object o = super.visit(node, data);
172                 setClassID(formerID);
173                 return o;
174             } else {
175                 String interfaceName = node.getImage();
176                 classDataList.clear();
177                 setClassID(0);
178                 classDataList.add(getClassID(), new ClassData(interfaceName));
179                 Object o = super.visit(node, data);
180                 if (o != null) {
181                     processRule(o);
182                 } else {
183                     processRule(data);
184                 }
185                 setClassID(-1);
186                 return o;
187             }
188         } else if (!(node.jjtGetParent().jjtGetParent() instanceof ASTCompilationUnit)) {
189             // not a top level class
190             int formerID = getClassID();
191             setClassID(classDataList.size());
192             // TODO
193             // this is a hack to bail out here
194             // but I'm not sure why this is happening
195             // TODO
196             if (formerID == -1 || formerID >= classDataList.size()) {
197                 return null;
198             }
199             // store the names of any outer classes of this class in the
200             // classQualifyingName List
201             ClassData formerClassData = classDataList.get(formerID);
202             String className = node.getImage();
203             ClassData newClassData = new ClassData(className);
204             newClassData.addClassQualifyingName(formerClassData.getClassName());
205             classDataList.add(getClassID(), newClassData);
206             Object o = super.visit(node, data);
207             setClassID(formerID);
208             return o;
209         }
210         // outer classes
211         if (!node.isStatic()) { // See bug# 1807370
212             String className = node.getImage();
213             classDataList.clear();
214             setClassID(0);// first class
215             classDataList.add(getClassID(), new ClassData(className));
216         }
217         Object o = super.visit(node, data);
218         if (o != null && !node.isStatic()) { // See bug# 1807370
219             processRule(o);
220         } else {
221             processRule(data);
222         }
223         setClassID(-1);
224         return o;
225     }
226 
227     /**
228      * Store all target constructors
229      */
230     public Object visit(ASTConstructorDeclaration node, Object data) {
231         if (node.isPrivate()) {
232             getCurrentClassData().addConstructor(node);
233         }
234         return super.visit(node, data);
235     }
236 
237     public Object visit(ASTAllocationExpression node, Object data) {
238         // TODO
239         // this is a hack to bail out here
240         // but I'm not sure why this is happening
241         // TODO
242         if (classID == -1 || getCurrentClassData() == null) {
243             return data;
244         }
245         AllocData ad = new AllocData(node, packageName, getCurrentClassData().getClassQualifyingNamesList());
246         if (!ad.isArray()) {
247             getCurrentClassData().addInstantiation(ad);
248         }
249         return super.visit(node, data);
250     }
251 
252     private void processRule(Object ctx) {
253         // check constructors of outerIterator against allocations of
254         // innerIterator
255         for (ClassData outerDataSet : classDataList) {
256             for (Iterator<ASTConstructorDeclaration> constructors = outerDataSet.getPrivateConstructorIterator(); constructors
257                     .hasNext();) {
258                 ASTConstructorDeclaration cd = constructors.next();
259 
260                 for (ClassData innerDataSet : classDataList) {
261                     if (outerDataSet.equals(innerDataSet)) {
262                         continue;
263                     }
264                     for (Iterator<AllocData> allocations = innerDataSet.getInstantiationIterator(); allocations
265                             .hasNext();) {
266                         AllocData ad = allocations.next();
267                         // if the constructor matches the instantiation
268                         // flag the instantiation as a generator of an extra
269                         // class
270                         if (outerDataSet.getClassName().equals(ad.getName())
271                                 && cd.getParameterCount() == ad.getArgumentCount()) {
272                             addViolation(ctx, ad.getASTAllocationExpression());
273                         }
274                     }
275                 }
276             }
277         }
278     }
279 
280     private ClassData getCurrentClassData() {
281         // TODO
282         // this is a hack to bail out here
283         // but I'm not sure why this is happening
284         // TODO
285         if (classID >= classDataList.size()) {
286             return null;
287         }
288         return classDataList.get(classID);
289     }
290 
291     private void setClassID(int id) {
292         classID = id;
293     }
294 
295     private int getClassID() {
296         return classID;
297     }
298 
299     // remove = Fire.
300     // value = someFire.Fighter
301     // 0123456789012345
302     // index = 4
303     // remove.size() = 5
304     // value.substring(0,4) = some
305     // value.substring(4 + remove.size()) = Fighter
306     // return "someFighter"
307 
308     // TODO move this into StringUtil
309     private static String stripString(String remove, String value) {
310         String returnValue;
311         int index = value.indexOf(remove);
312         if (index != -1) { // if the package name can start anywhere but 0
313                            // please inform the author because this will break
314             returnValue = value.substring(0, index) + value.substring(index + remove.length());
315         } else {
316             returnValue = value;
317         }
318         return returnValue;
319     }
320 
321 }