diff --git a/src/main/java/com/puppycrawl/tools/checkstyle/xpath/XpathQueryGenerator.java b/src/main/java/com/puppycrawl/tools/checkstyle/xpath/XpathQueryGenerator.java new file mode 100644 index 000000000000..85f01d23402f --- /dev/null +++ b/src/main/java/com/puppycrawl/tools/checkstyle/xpath/XpathQueryGenerator.java @@ -0,0 +1,233 @@ +//////////////////////////////////////////////////////////////////////////////// +// checkstyle: Checks Java source code for adherence to a set of rules. +// Copyright (C) 2001-2017 the original author or authors. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//////////////////////////////////////////////////////////////////////////////// + +package com.puppycrawl.tools.checkstyle.xpath; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; +import com.puppycrawl.tools.checkstyle.utils.TokenUtils; + +/** + * Generates xpath queries. Xpath queries are generated based on received + * {@code DetailAst} element, line number and column number. + * + *

+ * Example class + *

+ *
+ * public class Main {
+ *
+ *     public String sayHello(String name) {
+ *         return "Hello, " + name;
+ *     }
+ * }
+ * 
+ * + *

+ * Following expression returns list of queries. Each query is the string representing full + * path to the node inside Xpath tree, whose line number is 3 and column number is 4. + *

+ *
+ *     new XpathQueryGenerator(rootAst, 3, 4).generate();
+ * 
+ * + *

+ * Result list + *

+ * + * + * @author Timur Tibeyev. + */ +public class XpathQueryGenerator { + /** The root ast. */ + private final DetailAST rootAst; + /** The line number of the element for which the query should be generated. */ + private final int lineNumber; + /** The column number of the element for which the query should be generated. */ + private final int columnNumber; + + /** + * Creates a new {@code XpathQueryGenerator} instance. + * + * @param rootAst root ast + * @param lineNumber line number of the element for which the query should be generated + * @param columnNumber column number of the element for which the query should be generated + */ + public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber) { + this.rootAst = rootAst; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + /** + * Returns list of xpath queries of nodes, matching line and column number. + * This approach uses DetailAST traversal. DetailAST means detail abstract syntax tree. + * @return list of xpath queries of nodes, matching line and column number + */ + public List generate() { + return getMatchingAstElements(rootAst, lineNumber, columnNumber) + .stream() + .map(XpathQueryGenerator::generateXpathQuery) + .collect(Collectors.toList()); + } + + /** + * Returns child {@code DetailAst} element of the given root, + * which has child element with token type equals to {@link TokenTypes#IDENT}. + * @param root {@code DetailAST} root ast + * @return child {@code DetailAst} element of the given root + */ + private static DetailAST findChildWithIdent(DetailAST root) { + return TokenUtils.findFirstTokenByPredicate(root, + cur -> { + return cur.findFirstToken(TokenTypes.IDENT) != null; + }).orElse(null); + } + + /** + * Returns full xpath query for given ast element. + * @param ast {@code DetailAST} ast element + * @return full xpath query for given ast element + */ + private static String generateXpathQuery(DetailAST ast) { + String xpathQuery = getXpathQuery(null, ast); + if (!isUniqueAst(ast)) { + final DetailAST child = findChildWithIdent(ast); + if (child != null) { + xpathQuery += "[." + getXpathQuery(ast, child) + ']'; + } + } + return xpathQuery; + } + + /** + * Returns list of nodes matching defined line and column number. + * @param root {@code DetailAST} root ast + * @param lineNumber line number + * @param columnNumber column number + * @return list of nodes matching defined line and column number + */ + private static List getMatchingAstElements(DetailAST root, int lineNumber, + int columnNumber) { + final List result = new ArrayList<>(); + DetailAST curNode = root; + while (curNode != null && curNode.getLineNo() <= lineNumber) { + if (curNode.getLineNo() == lineNumber + && curNode.getColumnNo() == columnNumber + && curNode.getType() != TokenTypes.IDENT) { + result.add(curNode); + } + DetailAST toVisit = curNode.getFirstChild(); + while (curNode != null && toVisit == null) { + toVisit = curNode.getNextSibling(); + if (toVisit == null) { + curNode = curNode.getParent(); + } + } + + curNode = toVisit; + } + return result; + } + + /** + * Returns relative xpath query for given ast element from root. + * @param root {@code DetailAST} root element + * @param ast {@code DetailAST} ast element + * @return relative xpath query for given ast element from root + */ + private static String getXpathQuery(DetailAST root, DetailAST ast) { + final StringBuilder resultBuilder = new StringBuilder(1024); + DetailAST cur = ast; + while (cur != root) { + final StringBuilder curNodeQueryBuilder = new StringBuilder(256); + curNodeQueryBuilder.append('/') + .append(TokenUtils.getTokenName(cur.getType())); + final DetailAST identAst = cur.findFirstToken(TokenTypes.IDENT); + if (identAst != null) { + curNodeQueryBuilder.append("[@text='") + .append(identAst.getText()) + .append("']"); + } + resultBuilder.insert(0, curNodeQueryBuilder); + cur = cur.getParent(); + } + return resultBuilder.toString(); + } + + /** + * Checks if the given ast element has unique {@code TokenTypes} among siblings. + * @param ast {@code DetailAST} ast element + * @return if the given ast element has unique {@code TokenTypes} among siblings + */ + private static boolean hasAtLeastOneSiblingWithSameTokenType(DetailAST ast) { + boolean result = false; + if (ast.getParent() == null) { + DetailAST prev = ast.getPreviousSibling(); + while (prev != null) { + if (prev.getType() == ast.getType()) { + result = true; + break; + } + prev = prev.getPreviousSibling(); + } + if (!result) { + DetailAST next = ast.getNextSibling(); + while (next != null) { + if (next.getType() == ast.getType()) { + result = true; + break; + } + next = next.getNextSibling(); + } + } + } + else { + result = ast.getParent().getChildCount(ast.getType()) > 1; + } + return result; + } + + /** + * To be sure that generated xpath query will return exactly required ast element, the element + * should be checked for uniqueness. If ast element has {@link TokenTypes#IDENT} as the child + * or there is no sibling with the same {@code TokenTypes} then element is supposed to be + * unique. This method finds if {@code DetailAst} element is unique. + * @param ast {@code DetailAST} ast element + * @return if {@code DetailAst} element is unique + */ + private static boolean isUniqueAst(DetailAST ast) { + return ast.findFirstToken(TokenTypes.IDENT) != null + || !hasAtLeastOneSiblingWithSameTokenType(ast); + } +} diff --git a/src/main/java/com/puppycrawl/tools/checkstyle/xpath/package-info.java b/src/main/java/com/puppycrawl/tools/checkstyle/xpath/package-info.java index 6a7b75863932..e14213951f96 100644 --- a/src/main/java/com/puppycrawl/tools/checkstyle/xpath/package-info.java +++ b/src/main/java/com/puppycrawl/tools/checkstyle/xpath/package-info.java @@ -18,6 +18,6 @@ //////////////////////////////////////////////////////////////////////////////// /** - * Contains the nodes implementations for XPATH queries. + * Contains the nodes implementations for XPATH queries and query generator. */ package com.puppycrawl.tools.checkstyle.xpath; diff --git a/src/test/java/com/puppycrawl/tools/checkstyle/xpath/XpathQueryGeneratorTest.java b/src/test/java/com/puppycrawl/tools/checkstyle/xpath/XpathQueryGeneratorTest.java new file mode 100644 index 000000000000..548ab5ef04d0 --- /dev/null +++ b/src/test/java/com/puppycrawl/tools/checkstyle/xpath/XpathQueryGeneratorTest.java @@ -0,0 +1,306 @@ +//////////////////////////////////////////////////////////////////////////////// +// checkstyle: Checks Java source code for adherence to a set of rules. +// Copyright (C) 2001-2017 the original author or authors. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//////////////////////////////////////////////////////////////////////////////// + +package com.puppycrawl.tools.checkstyle.xpath; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.internal.TestUtils; + +public class XpathQueryGeneratorTest { + + private static DetailAST rootAst; + + @Before + public void init() throws Exception { + final File file = new File("src/test/resources/com/puppycrawl/tools/" + + "checkstyle/xpath/InputXpathQueryGenerator.java"); + rootAst = TestUtils.parseFile(file); + } + + @Test + public void testClassDef() { + final int lineNumber = 11; + final int columnNumber = 0; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/MODIFIERS", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/MODIFIERS/LITERAL_PUBLIC"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testMethodDef() { + final int lineNumber = 44; + final int columnNumber = 4; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']" + + "/OBJBLOCK/METHOD_DEF[@text='callSomeMethod']", + "/CLASS_DEF[@text='InputXpathQueryGenerator']" + + "/OBJBLOCK/METHOD_DEF[@text='callSomeMethod']/MODIFIERS", + "/CLASS_DEF[@text='InputXpathQueryGenerator']" + + "/OBJBLOCK/METHOD_DEF[@text='callSomeMethod']/MODIFIERS/LITERAL_PUBLIC"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testVariableDef() { + final int lineNumber = 52; + final int columnNumber = 12; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/SLIST/LITERAL_FOR" + + "/SLIST/VARIABLE_DEF[@text='d']", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/SLIST/LITERAL_FOR" + + "/SLIST/VARIABLE_DEF[@text='d']/MODIFIERS", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/SLIST/LITERAL_FOR" + + "/SLIST/VARIABLE_DEF[@text='d']/TYPE", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/SLIST/LITERAL_FOR" + + "/SLIST/VARIABLE_DEF[@text='d']/TYPE/LITERAL_SHORT"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testLcurly() { + final int lineNumber = 36; + final int columnNumber = 19; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK/METHOD_DEF[@text='Label']" + + "/SLIST/LITERAL_SWITCH/LCURLY"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testRcurly() { + final int lineNumber = 24; + final int columnNumber = 4; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK/INSTANCE_INIT" + + "/SLIST/RCURLY"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testExpr() { + final int lineNumber = 16; + final int columnNumber = 49; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/VARIABLE_DEF[@text='mUse4']/ASSIGN/EXPR", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/VARIABLE_DEF[@text='mUse4']/ASSIGN/EXPR/DOT"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testLparen() { + final int lineNumber = 44; + final int columnNumber = 30; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/LPAREN"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testEmpty() { + final int lineNumber = 300; + final int columnNumber = 300; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + assertTrue("Result should be empty", actual.isEmpty()); + } + + @Test + public void testPackage() { + final int lineNumber = 1; + final int columnNumber = 0; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/PACKAGE_DEF"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testImport() { + final int lineNumber = 4; + final int columnNumber = 0; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/IMPORT[./DOT[@text='File']]"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testMethodParams() { + final int lineNumber = 71; + final int columnNumber = 29; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='saveUser']/PARAMETERS", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='saveUser']/PARAMETERS/PARAMETER_DEF[@text='name']", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='saveUser']/PARAMETERS/PARAMETER_DEF[@text='name']" + + "/MODIFIERS", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='saveUser']/PARAMETERS/PARAMETER_DEF[@text='name']" + + "/TYPE[@text='String']"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testSwitch() { + final int lineNumber = 36; + final int columnNumber = 8; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='Label']/SLIST/LITERAL_SWITCH"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testSwitchCase() { + final int lineNumber = 37; + final int columnNumber = 12; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK/METHOD_DEF[@text='Label']" + + "/SLIST/LITERAL_SWITCH/CASE_GROUP", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK/METHOD_DEF[@text='Label']" + + "/SLIST/LITERAL_SWITCH/CASE_GROUP/LITERAL_DEFAULT"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testVariableStringLiteral() { + final int lineNumber = 46; + final int columnNumber = 25; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/SLIST/VARIABLE_DEF[@text='another']" + + "/ASSIGN/EXPR", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='callSomeMethod']/SLIST/VARIABLE_DEF[@text='another']" + + "/ASSIGN/EXPR/STRING_LITERAL"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testComma() { + final int lineNumber = 65; + final int columnNumber = 35; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK/METHOD_DEF[@text='foo']" + + "/SLIST/LITERAL_FOR/FOR_ITERATOR/ELIST/COMMA"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testLiteralVoid() { + final int lineNumber = 64; + final int columnNumber = 11; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Arrays.asList( + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='foo']/TYPE", + "/CLASS_DEF[@text='InputXpathQueryGenerator']/OBJBLOCK" + + "/METHOD_DEF[@text='foo']/TYPE/LITERAL_VOID"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testFirstImport() { + final int lineNumber = 3; + final int columnNumber = 0; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/IMPORT[./DOT[@text='JToolBar']]"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } + + @Test + public void testLastImport() { + final int lineNumber = 7; + final int columnNumber = 0; + final XpathQueryGenerator queryGenerator = new XpathQueryGenerator(rootAst, lineNumber, + columnNumber); + final List actual = queryGenerator.generate(); + final List expected = Collections.singletonList( + "/IMPORT[./DOT[@text='Iterator']]"); + assertEquals("Generated queries do not match expected ones", expected, actual); + } +} diff --git a/src/test/resources/com/puppycrawl/tools/checkstyle/xpath/InputXpathQueryGenerator.java b/src/test/resources/com/puppycrawl/tools/checkstyle/xpath/InputXpathQueryGenerator.java new file mode 100644 index 000000000000..7dbcc8cb3824 --- /dev/null +++ b/src/test/resources/com/puppycrawl/tools/checkstyle/xpath/InputXpathQueryGenerator.java @@ -0,0 +1,74 @@ +package com.puppycrawl.tools.checkstyle.xpath; + +import javax.swing.JToolBar; +import java.io.File; +import java.sql.Connection; +import java.util.Arrays; +import java.util.Iterator; + +import static java.io.File.listRoots; + +public class InputXpathQueryGenerator { + + private Class mUse1 = Connection.class; + private Class mUse2 = java.io.File.class; + private Class mUse3 = Iterator[].class; + private Class mUse4 = java.util.Enumeration[].class; + private String ftpClient = null; + + { + int[] x = {}; + Arrays.sort(x); + Object obj = javax.swing.BorderFactory.createEmptyBorder(); + File[] files = listRoots(); + } + + private JToolBar.Separator mSep = null; + + private Object mUse5 = new Object(); + + private Object mUse6 = new javax.swing.JToggleButton.ToggleButtonModel(); + + private int Component; + + public void Label() { + int i = 23; + switch (i) { + default: + break; + case 1: + break; + } + } + + public void callSomeMethod() { + int variable = 123; + String another = "HelloWorld"; + String[] array = new String[3]; + for (String cycle : array) { + char a = 'b'; + char b = a; + byte c = 1; + short d = 1; + } + } + + /** + * Returns if current node has children. + * @return if current node has children + */ + public String getSomeMethod() { + return "HelloWorld"; + } + + static void foo() { + for (int i = 0; i < 10; i++, i+=2) { + + } + return; + } + + private boolean saveUser(String name, String surname, int age) { + return true; + } +}