001: /**
002: * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003: */package net.sourceforge.pmd.rules;
004:
005: import java.util.ArrayList;
006: import java.util.HashMap;
007: import java.util.Iterator;
008: import java.util.List;
009: import java.util.Map;
010: import java.util.Stack;
011: import java.util.Map.Entry;
012:
013: import net.sourceforge.pmd.AbstractJavaRule;
014: import net.sourceforge.pmd.RuleContext;
015: import net.sourceforge.pmd.ast.ASTVariableDeclaratorId;
016: import net.sourceforge.pmd.ast.Node;
017: import net.sourceforge.pmd.ast.SimpleNode;
018: import net.sourceforge.pmd.jaxen.DocumentNavigator;
019: import net.sourceforge.pmd.jaxen.MatchesFunction;
020: import net.sourceforge.pmd.jaxen.TypeOfFunction;
021:
022: import org.jaxen.BaseXPath;
023: import org.jaxen.JaxenException;
024: import org.jaxen.SimpleVariableContext;
025: import org.jaxen.XPath;
026: import org.jaxen.expr.AllNodeStep;
027: import org.jaxen.expr.DefaultXPathFactory;
028: import org.jaxen.expr.Expr;
029: import org.jaxen.expr.LocationPath;
030: import org.jaxen.expr.NameStep;
031: import org.jaxen.expr.Predicate;
032: import org.jaxen.expr.Step;
033: import org.jaxen.expr.UnionExpr;
034: import org.jaxen.expr.XPathFactory;
035: import org.jaxen.saxpath.Axis;
036:
037: /**
038: * Rule that tries to match an XPath expression against a DOM
039: * view of the AST of a "compilation unit".
040: * <p/>
041: * This rule needs a property "xpath".
042: */
043: public class XPathRule extends AbstractJavaRule {
044:
045: // Mapping from Node name to applicable XPath queries
046: private Map<String, List<XPath>> nodeNameToXPaths;
047: private boolean regexpFunctionRegistered;
048: private boolean typeofFunctionRegistered;
049:
050: private static final String AST_ROOT = "_AST_ROOT_";
051:
052: /**
053: * Evaluate the AST with compilationUnit as root-node, against
054: * the XPath expression found as property with name "xpath".
055: * All matches are reported as violations.
056: *
057: * @param compilationUnit the Node that is the root of the AST to be checked
058: * @param data
059: */
060: public void evaluate(Node compilationUnit, RuleContext data) {
061: try {
062: initializeXPathExpression();
063: List<XPath> xpaths = nodeNameToXPaths.get(compilationUnit
064: .toString());
065: if (xpaths == null) {
066: xpaths = nodeNameToXPaths.get(AST_ROOT);
067: }
068: for (XPath xpath : xpaths) {
069: List results = xpath.selectNodes(compilationUnit);
070: for (Iterator j = results.iterator(); j.hasNext();) {
071: SimpleNode n = (SimpleNode) j.next();
072: // FUTURE Figure out a way to make adding a violation be AST independent, so XPathRule can be used on AST for any language.
073: if (n instanceof ASTVariableDeclaratorId
074: && getBooleanProperty("pluginname")) {
075: addViolation(data, n, n.getImage());
076: } else {
077: addViolation(data, n, getMessage());
078: }
079: }
080: }
081: } catch (JaxenException ex) {
082: throw new RuntimeException(ex);
083: }
084: }
085:
086: public List<String> getRuleChainVisits() {
087: try {
088: initializeXPathExpression();
089: return super .getRuleChainVisits();
090: } catch (JaxenException ex) {
091: throw new RuntimeException(ex);
092: }
093: }
094:
095: private void initializeXPathExpression() throws JaxenException {
096: if (nodeNameToXPaths != null) {
097: return;
098: }
099:
100: if (!regexpFunctionRegistered) {
101: MatchesFunction.registerSelfInSimpleContext();
102: regexpFunctionRegistered = true;
103: }
104:
105: if (!typeofFunctionRegistered) {
106: TypeOfFunction.registerSelfInSimpleContext();
107: typeofFunctionRegistered = true;
108: }
109:
110: //
111: // Attempt to use the RuleChain with this XPath query. To do so, the queries
112: // should generally look like //TypeA or //TypeA | //TypeB. We will look at the
113: // parsed XPath AST using the Jaxen APIs to make this determination.
114: // If the query is not exactly what we are looking for, do not use the RuleChain.
115: //
116: nodeNameToXPaths = new HashMap<String, List<XPath>>();
117:
118: BaseXPath originalXPath = createXPath(getStringProperty("xpath"));
119: indexXPath(originalXPath, AST_ROOT);
120:
121: boolean useRuleChain = true;
122: Stack<Expr> pending = new Stack<Expr>();
123: pending.push(originalXPath.getRootExpr());
124: while (!pending.isEmpty()) {
125: Expr node = pending.pop();
126:
127: // Need to prove we can handle this part of the query
128: boolean valid = false;
129:
130: // Must be a LocationPath... that is something like //Type
131: if (node instanceof LocationPath) {
132: LocationPath locationPath = (LocationPath) node;
133: if (locationPath.isAbsolute()) {
134: // Should be at least two steps
135: List steps = locationPath.getSteps();
136: if (steps.size() >= 2) {
137: Step step1 = (Step) steps.get(0);
138: Step step2 = (Step) steps.get(1);
139: // First step should be an AllNodeStep using the descendant or self axis
140: if (step1 instanceof AllNodeStep
141: && ((AllNodeStep) step1).getAxis() == Axis.DESCENDANT_OR_SELF) {
142: // Second step should be a NameStep using the child axis.
143: if (step2 instanceof NameStep
144: && ((NameStep) step2).getAxis() == Axis.CHILD) {
145: // Construct a new expression that is appropriate for RuleChain use
146: XPathFactory xpathFactory = new DefaultXPathFactory();
147:
148: // Instead of an absolute location path, we'll be using a relative path
149: LocationPath relativeLocationPath = xpathFactory
150: .createRelativeLocationPath();
151: // The first step will be along the self axis
152: Step allNodeStep = xpathFactory
153: .createAllNodeStep(Axis.SELF);
154: // Retain all predicates from the original name step
155: for (Iterator i = step2.getPredicates()
156: .iterator(); i.hasNext();) {
157: allNodeStep
158: .addPredicate((Predicate) i
159: .next());
160: }
161: relativeLocationPath
162: .addStep(allNodeStep);
163:
164: // Retain the remaining steps from the original location path
165: for (int i = 2; i < steps.size(); i++) {
166: relativeLocationPath
167: .addStep((Step) steps
168: .get(i));
169: }
170:
171: BaseXPath xpath = createXPath(relativeLocationPath
172: .getText());
173: indexXPath(xpath, ((NameStep) step2)
174: .getLocalName());
175: valid = true;
176: }
177: }
178: }
179: }
180: } else if (node instanceof UnionExpr) { // Or a UnionExpr, that is something like //TypeA | //TypeB
181: UnionExpr unionExpr = (UnionExpr) node;
182: pending.push(unionExpr.getLHS());
183: pending.push(unionExpr.getRHS());
184: valid = true;
185: }
186: if (!valid) {
187: useRuleChain = false;
188: break;
189: }
190: }
191:
192: if (useRuleChain) {
193: // Use the RuleChain for all the nodes extracted from the xpath queries
194: for (String s : nodeNameToXPaths.keySet()) {
195: addRuleChainVisit(s);
196: }
197: } else { // Use original XPath if we cannot use the rulechain
198: nodeNameToXPaths.clear();
199: indexXPath(originalXPath, AST_ROOT);
200: //System.err.println("Unable to use RuleChain for " + this.getName() + " for XPath: " + getStringProperty("xpath"));
201: }
202: }
203:
204: private void indexXPath(XPath xpath, String nodeName) {
205: List<XPath> xpaths = nodeNameToXPaths.get(nodeName);
206: if (xpaths == null) {
207: xpaths = new ArrayList<XPath>();
208: nodeNameToXPaths.put(nodeName, xpaths);
209: }
210: xpaths.add(xpath);
211: }
212:
213: private BaseXPath createXPath(String xpathQueryString)
214: throws JaxenException {
215: // TODO As of Jaxen 1.1, LiteralExpr which contain " or ' characters
216: // are not escaped properly. The following is fix for the known
217: // XPath queries built into PMD. It will not necessarily work for
218: // arbitrary XPath queries users of PMD will create. JAXEN-177 is
219: // about this problem: http://jira.codehaus.org/browse/JAXEN-177
220: // PMD should upgrade to the next Jaxen release containing this fix.
221: xpathQueryString = xpathQueryString
222: .replaceAll("\"\"\"", "'\"'");
223:
224: BaseXPath xpath = new BaseXPath(xpathQueryString,
225: new DocumentNavigator());
226: if (getProperties().size() > 1) {
227: SimpleVariableContext vc = new SimpleVariableContext();
228: for (Entry e : getProperties().entrySet()) {
229: if (!"xpath".equals(e.getKey())) {
230: vc.setVariableValue((String) e.getKey(), e
231: .getValue());
232: }
233: }
234: xpath.setVariableContext(vc);
235: }
236: return xpath;
237: }
238:
239: /**
240: * Apply the rule to all compilation units.
241: */
242: public void apply(List astCompilationUnits, RuleContext ctx) {
243: for (Iterator i = astCompilationUnits.iterator(); i.hasNext();) {
244: evaluate((Node) i.next(), ctx);
245: }
246: }
247: }
|