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 }