////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2015 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;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.beanutils.PropertyUtils;
import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.google.common.io.Files;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.Check;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
public class XDocsPagesTest {
private static final File JAVA_SOURCES_DIRECTORY = new File("src/main/java");
private static final File XDOCS_DIRECTORY = new File("src/xdocs");
private static final String AVAILABLE_CHECKS_PATH = "src/xdocs/checks.xml";
private static final File AVAILABLE_CHECKS_FILE = new File(AVAILABLE_CHECKS_PATH);
private static final String CHECK_FILE_NAME = ".+Check.java$";
private static final String CHECK_SUFFIX = "Check.java";
private static final String LINK_TEMPLATE =
"(?s).*%1$s.*";
private static final List CHECKS_ON_PAGE_IGNORE_LIST = Arrays.asList(
"AbstractAccessControlNameCheck.java",
"AbstractClassCouplingCheck.java",
"AbstractComplexityCheck.java",
"AbstractFileSetCheck.java",
"AbstractFormatCheck.java",
"AbstractHeaderCheck.java",
"AbstractIllegalCheck.java",
"AbstractIllegalMethodCheck.java",
"AbstractJavadocCheck.java",
"AbstractNameCheck.java",
"AbstractNestedDepthCheck.java",
"AbstractOptionCheck.java",
"AbstractParenPadCheck.java",
"AbstractSuperCheck.java",
"AbstractTypeAwareCheck.java",
"AbstractTypeParameterNameCheck.java",
"FileSetCheck.java"
);
private static final List XML_FILESET_LIST = Arrays.asList(
"TreeWalker",
"name=\"Checker\"",
"name=\"Header\"",
"name=\"Translation\"",
"name=\"SeverityMatchFilter\"",
"name=\"SuppressionFilter\"",
"name=\"SuppressionCommentFilter\"",
"name=\"SuppressWithNearbyCommentFilter\"",
"name=\"SuppressWarningsFilter\"",
"name=\"RegexpHeader\"",
"name=\"RegexpSingleline\"",
"name=\"RegexpMultiline\"",
"name=\"JavadocPackage\"",
"name=\"NewlineAtEndOfFile\"",
"name=\"UniqueProperties\"",
"name=\"FileLength\"",
"name=\"FileTabCharacter\""
);
private static final Set CHECK_PROPERTIES = getProperties(Check.class);
private static final Set FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class);
private static final List UNDOCUMENTED_PROPERTIES = Arrays.asList(
"SuppressWithNearbyCommentFilter.fileContents",
"SuppressionCommentFilter.fileContents"
);
@Test
public void testAllChecksPresentOnAvailableChecksPage() throws IOException {
final String availableChecks = Files.toString(AVAILABLE_CHECKS_FILE, UTF_8);
for (File file : Files.fileTreeTraverser().preOrderTraversal(JAVA_SOURCES_DIRECTORY)) {
final String fileName = file.getName();
if (fileName.matches(CHECK_FILE_NAME)
&& !CHECKS_ON_PAGE_IGNORE_LIST.contains(fileName)) {
final String checkName = fileName.replace(CHECK_SUFFIX, "");
if (!isPresent(availableChecks, checkName)) {
Assert.fail(checkName + " is not correctly listed on Available Checks page"
+ " - add it to " + AVAILABLE_CHECKS_PATH);
}
}
}
}
private static boolean isPresent(String availableChecks, String checkName) {
final String linkPattern = String.format(Locale.ROOT, LINK_TEMPLATE, checkName);
return availableChecks.matches(linkPattern);
}
@Test
public void testAllXmlExamples() throws Exception {
for (File file : Files.fileTreeTraverser().preOrderTraversal(XDOCS_DIRECTORY)) {
if (file.isDirectory()) {
continue;
}
final String input = Files.toString(file, UTF_8);
final String fileName = file.getName();
final Document document = getRawXml(fileName, input, input);
final NodeList sources = document.getElementsByTagName("source");
for (int position = 0; position < sources.getLength(); position++) {
final String unserializedSource = sources.item(position).getTextContent()
.replace("...", "").trim();
if (unserializedSource.charAt(0) != '<'
|| unserializedSource.charAt(unserializedSource.length() - 1) != '>') {
continue;
}
// no dtd testing yet
if (unserializedSource.contains("\n" + code + "\n";
}
if (!code.contains("name=\"Checker\"")) {
code = "\n" + code + "\n";
}
if (!code.startsWith("\n\n"
+ code;
}
// test only
getRawXml(fileName, code, unserializedSource);
// can't test ant structure, or old and outdated checks
if (!fileName.startsWith("anttask") && !fileName.startsWith("releasenotes")) {
testCheckstyleXml(fileName, code, unserializedSource);
}
}
private static boolean hasFileSetClass(String xml) {
boolean found = false;
for (String find : XML_FILESET_LIST) {
if (xml.contains(find)) {
found = true;
break;
}
}
return found;
}
private static Document getRawXml(String fileName, String code, String unserializedSource)
throws ParserConfigurationException {
try {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setNamespaceAware(true);
final DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new InputSource(new StringReader(code)));
}
catch (IOException | SAXException e) {
Assert.fail(fileName + " has invalid xml (" + e.getMessage() + "): "
+ unserializedSource);
}
return null;
}
private static void testCheckstyleXml(String fileName, String code, String unserializedSource)
throws IOException {
// can't process non-existent examples, or out of context snippets
if (code.contains("com.mycompany") || code.contains("checkstyle-packages")
|| code.contains("MethodLimit") || code.contains(" packageNames = PackageNamesLoader.getPackageNames(cl);
final ModuleFactory moduleFactory = new PackageObjectFactory(packageNames, cl);
for (File file : Files.fileTreeTraverser().preOrderTraversal(XDOCS_DIRECTORY)) {
final String fileName = file.getName();
if (!fileName.startsWith("config_") || "config_reporting.xml".equals(fileName)) {
continue;
}
final String input = Files.toString(file, UTF_8);
final Document document = getRawXml(fileName, input, input);
final NodeList sources = document.getElementsByTagName("section");
String lastSectioName = null;
for (int position = 0; position < sources.getLength(); position++) {
final Node section = sources.item(position);
final String sectionName = section.getAttributes().getNamedItem("name")
.getNodeValue();
if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
Assert.assertNull(fileName + " section '" + sectionName + "' should be first",
lastSectioName);
continue;
}
Assert.assertTrue(fileName + " section '" + sectionName
+ "' shouldn't end with 'Check'", !sectionName.endsWith("Check"));
if (lastSectioName != null) {
Assert.assertTrue(
fileName + " section '" + sectionName
+ "' is out of order compared to '" + lastSectioName + "'",
sectionName.toLowerCase(Locale.ENGLISH).compareTo(
lastSectioName.toLowerCase(Locale.ENGLISH)) >= 0);
}
validateCheckSection(moduleFactory, fileName, sectionName, section);
lastSectioName = sectionName;
}
}
}
private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
String sectionName, Node section) {
Object instance = null;
try {
instance = moduleFactory.createModule(sectionName);
}
catch (CheckstyleException e) {
Assert.fail(fileName + " couldn't find class: " + sectionName);
}
int subSectionPos = 0;
for (Node subSection : getChildrenElements(section)) {
final String subSectionName = subSection.getAttributes().getNamedItem("name")
.getNodeValue();
// can be in different orders, and completely optional
if ("Notes".equals(subSectionName)
|| "Rule Description".equals(subSectionName)) {
continue;
}
if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
validatePropertySection(fileName, sectionName, null, instance);
subSectionPos++;
}
Assert.assertEquals(fileName + " section '" + sectionName
+ "' should be in order", getSubSectionName(subSectionPos),
subSectionName);
switch (subSectionPos) {
case 0:
break;
case 1:
validatePropertySection(fileName, sectionName, subSection, instance);
break;
case 2:
break;
case 3:
validateUsageExample(fileName, sectionName, subSection);
break;
case 4:
validatePackageSection(fileName, sectionName, subSection, instance);
break;
case 5:
validateParentSection(fileName, sectionName, subSection);
break;
default:
break;
}
subSectionPos++;
}
}
private static Object getSubSectionName(int subSectionPos) {
final String result;
switch (subSectionPos) {
case 0:
result = "Description";
break;
case 1:
result = "Properties";
break;
case 2:
result = "Examples";
break;
case 3:
result = "Example of Usage";
break;
case 4:
result = "Package";
break;
case 5:
result = "Parent Module";
break;
default:
result = null;
break;
}
return result;
}
private static void validatePropertySection(String fileName, String sectionName,
Node subSection, Object instance) {
final Set properties = getProperties(instance.getClass());
final Class> clss = instance.getClass();
// remove global properties that don't need documentation
if (hasParentModule(sectionName)) {
properties.removeAll(CHECK_PROPERTIES);
}
else if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
properties.removeAll(FILESET_PROPERTIES);
// override
properties.add("fileExtensions");
}
// remove undocumented properties
for (String p : new HashSet<>(properties)) {
if (UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + p)) {
properties.remove(p);
}
}
final Check check;
if (Check.class.isAssignableFrom(clss)) {
check = (Check) instance;
if (!Arrays.equals(check.getAcceptableTokens(), check.getDefaultTokens())
|| !Arrays.equals(check.getAcceptableTokens(), check.getRequiredTokens())) {
properties.add("tokens");
}
}
else {
check = null;
}
if (subSection != null) {
Assert.assertTrue(fileName + " section '" + sectionName
+ "' should have no properties to show", !properties.isEmpty());
validatePropertySectionProperties(fileName, sectionName, subSection, check,
properties);
}
Assert.assertTrue(fileName + " section '" + sectionName + "' should show properties: "
+ properties, properties.isEmpty());
}
private static void validatePropertySectionProperties(String fileName, String sectionName,
Node subSection, Check check, Set properties) {
boolean skip = true;
boolean didTokens = false;
for (Node row : getChildrenElements(getFirstChildElement(subSection))) {
if (skip) {
skip = false;
continue;
}
Assert.assertFalse(fileName + " section '" + sectionName
+ "' should have token properties last", didTokens);
final List columns = new ArrayList<>(getChildrenElements(row));
final String propertyName = columns.get(0).getTextContent();
Assert.assertTrue(fileName + " section '" + sectionName
+ "' should not contain the property: " + propertyName,
properties.remove(propertyName));
if ("tokens".equals(propertyName)) {
Assert.assertEquals(fileName + " section '" + sectionName
+ "' should have the basic token description", "tokens to check",
columns.get(1).getTextContent());
Assert.assertEquals(fileName + " section '" + sectionName
+ "' should have all the acceptable tokens", "subset of tokens "
+ getTokenText(check.getAcceptableTokens(), check.getRequiredTokens()),
columns.get(2).getTextContent().replaceAll("\\s+", " ").trim());
Assert.assertEquals(fileName + " section '" + sectionName
+ "' should have all the default tokens",
getTokenText(check.getDefaultTokens(), check.getRequiredTokens()),
columns.get(3).getTextContent().replaceAll("\\s+", " ").trim());
didTokens = true;
}
else {
Assert.assertFalse(fileName + " section '" + sectionName
+ "' should have a description for " + propertyName, columns.get(1)
.getTextContent().trim().isEmpty());
Assert.assertFalse(fileName + " section '" + sectionName
+ "' should have a type for " + propertyName, columns.get(2)
.getTextContent().trim().isEmpty());
// default can be empty string
}
}
}
private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
final String text = subSection.getTextContent().replace("Checkstyle Style", "")
.replace("Google Style", "").replace("Sun Style", "").trim();
Assert.assertTrue(fileName + " section '" + sectionName
+ "' has unknown text in 'Example of Usage': " + text, text.isEmpty());
for (Node node : findChildElementsByTag(subSection, "a")) {
final String url = node.getAttributes().getNamedItem("href").getTextContent();
final String linkText = node.getTextContent().trim();
String expectedUrl = null;
if ("Checkstyle Style".equals(linkText)) {
expectedUrl = "https://github.com/search?q="
+ "path%3Aconfig+filename%3Acheckstyle_checks.xml+"
+ "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
}
else if ("Google Style".equals(linkText)) {
expectedUrl = "https://github.com/search?q="
+ "path%3Asrc%2Fmain%2Fresources+filename%3Agoogle_checks.xml+"
+ "repo%3Acheckstyle%2Fcheckstyle+"
+ sectionName;
}
else if ("Sun Style".equals(linkText)) {
expectedUrl = "https://github.com/search?q="
+ "path%3Asrc%2Fmain%2Fresources+filename%3Asun_checks.xml+"
+ "repo%3Acheckstyle%2Fcheckstyle+"
+ sectionName;
}
Assert.assertEquals(fileName + " section '" + sectionName
+ "' should have matching url", expectedUrl, url);
}
}
private static void validatePackageSection(String fileName, String sectionName,
Node subSection, Object instance) {
Assert.assertEquals(fileName + " section '" + sectionName
+ "' should have matching package", instance.getClass().getPackage().getName(),
subSection.getTextContent().trim());
}
private static void validateParentSection(String fileName, String sectionName,
Node subSection) {
final String expected;
if (hasParentModule(sectionName)) {
expected = "TreeWalker";
}
else {
expected = "Checker";
}
Assert.assertEquals(
fileName + " section '" + sectionName + "' should have matching parent",
expected, subSection
.getTextContent().trim());
}
private static Set getChildrenElements(Node node) {
final Set result = new LinkedHashSet<>();
for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() != Node.TEXT_NODE) {
result.add(child);
}
}
return result;
}
private static Node getFirstChildElement(Node node) {
for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() != Node.TEXT_NODE) {
return child;
}
}
return null;
}
private static Set findChildElementsByTag(Node node, String tag) {
final Set result = new LinkedHashSet<>();
for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
if (tag.equals(child.getNodeName())) {
result.add(child);
}
else if (child.hasChildNodes()) {
result.addAll(findChildElementsByTag(child, tag));
}
}
return result;
}
private static boolean hasParentModule(String sectionName) {
final String search = "\"" + sectionName + "\"";
boolean result = true;
for (String find : XML_FILESET_LIST) {
if (find.contains(search)) {
result = false;
break;
}
}
return result;
}
private static Set getProperties(Class> clss) {
final Set result = new TreeSet<>();
final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
for (PropertyDescriptor p : map) {
if (p.getWriteMethod() != null) {
result.add(p.getName());
}
}
return result;
}
private static String getTokenText(int[] tokens, int... subtractions) {
if (Arrays.equals(tokens, TokenUtils.getAllTokenIds()) && subtractions.length == 0) {
return "TokenTypes.";
}
else {
final StringBuilder result = new StringBuilder();
boolean first = true;
for (int token : tokens) {
boolean found = false;
for (int subtraction : subtractions) {
if (subtraction == token) {
found = true;
break;
}
}
if (found) {
continue;
}
if (first) {
first = false;
}
else {
result.append(", ");
}
result.append(TokenUtils.getTokenName(token));
}
if (result.length() == 0) {
result.append("empty");
}
else {
result.append(".");
}
return result.toString();
}
}
@Test
public void testAllStyleRules() throws Exception {
for (File file : Files.fileTreeTraverser().preOrderTraversal(XDOCS_DIRECTORY)) {
final String fileName = file.getName();
if (!fileName.endsWith("_style.xml")) {
continue;
}
final String input = Files.toString(file, UTF_8);
final Document document = getRawXml(fileName, input, input);
final NodeList sources = document.getElementsByTagName("tr");
String lastRuleName = null;
for (int position = 0; position < sources.getLength(); position++) {
final Node row = sources.item(position);
final List columns = new ArrayList<>(findChildElementsByTag(row, "td"));
if (columns.isEmpty()) {
continue;
}
final String ruleName = columns.get(1).getTextContent().trim();
if (lastRuleName != null) {
Assert.assertTrue(
fileName + " rule '" + ruleName + "' is out of order compared to '"
+ lastRuleName + "'",
ruleName.toLowerCase(Locale.ENGLISH).compareTo(
lastRuleName.toLowerCase(Locale.ENGLISH)) >= 0);
}
validateStyleChecks(findChildElementsByTag(columns.get(2), "a"),
findChildElementsByTag(columns.get(3), "a"), fileName, ruleName);
lastRuleName = ruleName;
}
}
}
private static void validateStyleChecks(Set checks, Set configs, String fileName,
String ruleName) {
final Iterator itrChecks = checks.iterator();
final Iterator itrConfigs = configs.iterator();
while (itrChecks.hasNext()) {
final Node check = itrChecks.next();
final String checkName = check.getTextContent().trim();
if (!check.getAttributes().getNamedItem("href").getTextContent()
.startsWith("config_")) {
continue;
}
Assert.assertTrue(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' shouldn't end with 'Check'", !checkName.endsWith("Check"));
for (String configName : new String[] {"config", "test"}) {
Node config = null;
try {
config = itrConfigs.next();
}
catch (NoSuchElementException ignore) {
Assert.fail(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' is missing the config link: " + configName);
}
Assert.assertEquals(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' has mismatched config/test links", configName, config.getTextContent()
.trim());
final String configUrl = config.getAttributes().getNamedItem("href")
.getTextContent();
if ("config".equals(configName)) {
final String expectedUrl = "https://github.com/search?q="
+ "path%3Asrc%2Fmain%2Fresources+filename%3Agoogle_checks.xml+"
+ "repo%3Acheckstyle%2Fcheckstyle+" + checkName;
Assert.assertEquals(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' should have matching " + configName + " url", expectedUrl,
configUrl);
}
else if ("test".equals(configName)) {
Assert.assertTrue(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' should have matching " + configName + " url",
configUrl.startsWith("https://github.com/checkstyle/checkstyle/"
+ "blob/master/src/it/java/com/google/checkstyle/test/"));
Assert.assertTrue(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' should have matching " + configName + " url",
configUrl.endsWith("/" + checkName + "Test.java"));
Assert.assertTrue(fileName + " rule '" + ruleName + "' check '" + checkName
+ "' should have a test that exists", new File(configUrl.substring(53)
.replace('/', File.separatorChar)).exists());
}
}
}
Assert.assertFalse(fileName + " rule '" + ruleName + "' has too many configs",
itrConfigs.hasNext());
}
}