View Javadoc

1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.rules;
5   
6   import java.util.ArrayList;
7   import java.util.HashMap;
8   import java.util.Iterator;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.Stack;
12  import java.util.Map.Entry;
13  
14  import net.sourceforge.pmd.AbstractJavaRule;
15  import net.sourceforge.pmd.RuleContext;
16  import net.sourceforge.pmd.ast.ASTVariableDeclaratorId;
17  import net.sourceforge.pmd.ast.Node;
18  import net.sourceforge.pmd.ast.SimpleNode;
19  import net.sourceforge.pmd.jaxen.DocumentNavigator;
20  import net.sourceforge.pmd.jaxen.MatchesFunction;
21  import net.sourceforge.pmd.jaxen.TypeOfFunction;
22  
23  import org.jaxen.BaseXPath;
24  import org.jaxen.JaxenException;
25  import org.jaxen.SimpleVariableContext;
26  import org.jaxen.XPath;
27  import org.jaxen.expr.AllNodeStep;
28  import org.jaxen.expr.DefaultXPathFactory;
29  import org.jaxen.expr.Expr;
30  import org.jaxen.expr.LocationPath;
31  import org.jaxen.expr.NameStep;
32  import org.jaxen.expr.Predicate;
33  import org.jaxen.expr.Step;
34  import org.jaxen.expr.UnionExpr;
35  import org.jaxen.expr.XPathFactory;
36  import org.jaxen.saxpath.Axis;
37  
38  /**
39   * Rule that tries to match an XPath expression against a DOM
40   * view of the AST of a "compilation unit".
41   * <p/>
42   * This rule needs a property "xpath".
43   */
44  public class XPathRule extends AbstractJavaRule {
45  
46      // Mapping from Node name to applicable XPath queries
47      private Map<String, List<XPath>> nodeNameToXPaths;
48      private boolean regexpFunctionRegistered;
49      private boolean typeofFunctionRegistered;
50  
51      private static final String AST_ROOT = "_AST_ROOT_";
52  
53      /**
54       * Evaluate the AST with compilationUnit as root-node, against
55       * the XPath expression found as property with name "xpath".
56       * All matches are reported as violations.
57       *
58       * @param compilationUnit the Node that is the root of the AST to be checked
59       * @param data
60       */
61      public void evaluate(Node compilationUnit, RuleContext data) {
62          try {
63              initializeXPathExpression();
64              List<XPath> xpaths = nodeNameToXPaths.get(compilationUnit.toString());
65              if (xpaths == null) {
66                  xpaths = nodeNameToXPaths.get(AST_ROOT);
67              }
68              for (XPath xpath: xpaths) {
69                  List results = xpath.selectNodes(compilationUnit);
70                  for (Iterator j = results.iterator(); j.hasNext();) {
71                      SimpleNode n = (SimpleNode) j.next();
72                      // FUTURE Figure out a way to make adding a violation be AST independent, so XPathRule can be used on AST for any language.
73                      if (n instanceof ASTVariableDeclaratorId && getBooleanProperty("pluginname")) {
74                          addViolation(data, n, n.getImage());
75                      } else {
76                          addViolation(data, n, getMessage());
77                      }
78                  }
79              }
80          } catch (JaxenException ex) {
81              throw new RuntimeException(ex);
82          }
83      }
84  
85      public List<String> getRuleChainVisits() {
86          try {
87              initializeXPathExpression();
88              return super.getRuleChainVisits();
89          } catch (JaxenException ex) {
90              throw new RuntimeException(ex);
91          }
92      }
93  
94      private void initializeXPathExpression() throws JaxenException {
95          if (nodeNameToXPaths != null) {
96              return;
97          }
98  
99          if (!regexpFunctionRegistered) {
100             MatchesFunction.registerSelfInSimpleContext();
101             regexpFunctionRegistered = true;
102         }
103 
104         if (!typeofFunctionRegistered) {
105             TypeOfFunction.registerSelfInSimpleContext();
106             typeofFunctionRegistered = true;
107         }
108 
109         //
110         // Attempt to use the RuleChain with this XPath query.  To do so, the queries
111         // should generally look like //TypeA or //TypeA | //TypeB.  We will look at the
112         // parsed XPath AST using the Jaxen APIs to make this determination.
113         // If the query is not exactly what we are looking for, do not use the RuleChain.
114         //
115         nodeNameToXPaths = new HashMap<String, List<XPath>>();
116 
117         BaseXPath originalXPath = createXPath(getStringProperty("xpath"));
118         indexXPath(originalXPath, AST_ROOT);
119 
120         boolean useRuleChain = true;
121         Stack<Expr> pending = new Stack<Expr>();
122         pending.push(originalXPath.getRootExpr());
123         while (!pending.isEmpty()) {
124             Expr node = pending.pop();
125 
126             // Need to prove we can handle this part of the query
127             boolean valid = false;
128 
129             // Must be a LocationPath... that is something like //Type
130             if (node instanceof LocationPath) {
131                 LocationPath locationPath = (LocationPath)node;
132                 if (locationPath.isAbsolute()) {
133                     // Should be at least two steps
134                     List steps = locationPath.getSteps();
135                     if (steps.size() >= 2) {
136                         Step step1 = (Step)steps.get(0);
137                         Step step2 = (Step)steps.get(1);
138                         // First step should be an AllNodeStep using the descendant or self axis
139                         if (step1 instanceof AllNodeStep && ((AllNodeStep)step1).getAxis() == Axis.DESCENDANT_OR_SELF) {
140                             // Second step should be a NameStep using the child axis.
141                             if (step2 instanceof NameStep && ((NameStep)step2).getAxis() == Axis.CHILD) {
142                                 // Construct a new expression that is appropriate for RuleChain use
143                                 XPathFactory xpathFactory = new DefaultXPathFactory();
144 
145                                 // Instead of an absolute location path, we'll be using a relative path
146                                 LocationPath relativeLocationPath = xpathFactory.createRelativeLocationPath();
147                                 // The first step will be along the self axis
148                                 Step allNodeStep = xpathFactory.createAllNodeStep(Axis.SELF);
149                                 // Retain all predicates from the original name step
150                                 for (Iterator i = step2.getPredicates().iterator(); i.hasNext();) {
151                                     allNodeStep.addPredicate((Predicate)i.next());
152                                 }
153                                 relativeLocationPath.addStep(allNodeStep);
154 
155                                 // Retain the remaining steps from the original location path
156                                 for (int i = 2; i < steps.size(); i++) {
157                                     relativeLocationPath.addStep((Step)steps.get(i));
158                                 }
159 
160                                 BaseXPath xpath = createXPath(relativeLocationPath.getText());
161                                 indexXPath(xpath, ((NameStep)step2).getLocalName());
162                                 valid = true;
163                             }
164                         }
165                     }
166                 }
167             } else if (node instanceof UnionExpr) { // Or a UnionExpr, that is something like //TypeA | //TypeB
168                 UnionExpr unionExpr = (UnionExpr)node;
169                 pending.push(unionExpr.getLHS());
170                 pending.push(unionExpr.getRHS());
171                 valid = true;
172             }
173             if (!valid) {
174                 useRuleChain = false;
175                 break;
176             }
177         }
178 
179         if (useRuleChain) {
180             // Use the RuleChain for all the nodes extracted from the xpath queries
181             for (String s: nodeNameToXPaths.keySet()) {
182                 addRuleChainVisit(s);
183             }
184         } else { // Use original XPath if we cannot use the rulechain
185             nodeNameToXPaths.clear();
186             indexXPath(originalXPath, AST_ROOT);
187             //System.err.println("Unable to use RuleChain for " + this.getName() + " for XPath: " + getStringProperty("xpath"));
188         }
189     }
190 
191     private void indexXPath(XPath xpath, String nodeName) {
192         List<XPath> xpaths = nodeNameToXPaths.get(nodeName);
193         if (xpaths == null) {
194             xpaths = new ArrayList<XPath>();
195             nodeNameToXPaths.put(nodeName, xpaths);
196         }
197         xpaths.add(xpath);
198     }
199 
200     private BaseXPath createXPath(String xpathQueryString) throws JaxenException {
201         // TODO As of Jaxen 1.1, LiteralExpr which contain " or ' characters
202         // are not escaped properly.  The following is fix for the known
203         // XPath queries built into PMD.  It will not necessarily work for
204         // arbitrary XPath queries users of PMD will create.  JAXEN-177 is
205         // about this problem: http://jira.codehaus.org/browse/JAXEN-177
206         // PMD should upgrade to the next Jaxen release containing this fix.
207         xpathQueryString = xpathQueryString.replaceAll("\"\"\"", "'\"'");
208 
209         BaseXPath xpath = new BaseXPath(xpathQueryString, new DocumentNavigator());
210         if (getProperties().size() > 1) {
211             SimpleVariableContext vc = new SimpleVariableContext();
212             for (Entry e: getProperties().entrySet()) {
213                 if (!"xpath".equals(e.getKey())) {
214                     vc.setVariableValue((String) e.getKey(), e.getValue());
215                 }
216             }
217             xpath.setVariableContext(vc);
218         }
219         return xpath;
220     }
221 
222     /**
223      * Apply the rule to all compilation units.
224      */
225     public void apply(List astCompilationUnits, RuleContext ctx) {
226         for (Iterator i = astCompilationUnits.iterator(); i.hasNext();) {
227             evaluate((Node) i.next(), ctx);
228         }
229     }
230 }