//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2016 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.internal; 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.lang.reflect.Field; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; 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 java.util.regex.Pattern; 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 com.puppycrawl.tools.checkstyle.Checker; import com.puppycrawl.tools.checkstyle.ConfigurationLoader; import com.puppycrawl.tools.checkstyle.ModuleFactory; import com.puppycrawl.tools.checkstyle.PropertiesExpander; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; import com.puppycrawl.tools.checkstyle.api.CheckstyleException; import com.puppycrawl.tools.checkstyle.api.Configuration; import com.puppycrawl.tools.checkstyle.api.Scope; import com.puppycrawl.tools.checkstyle.api.SeverityLevel; import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; public class XDocsPagesTest { private static final Path JAVA_SOURCES_DIRECTORY = Paths.get("src/main/java"); private static final Path AVAILABLE_CHECKS_PATH = Paths.get("src/xdocs/checks.xml"); private static final String CHECK_FILE_NAME = ".+Check.java$"; private static final String CHECK_SUFFIX = "Check.java"; private static final String LINK_TEMPLATE = "(?s).*<a href=\"config_\\w+\\.html#%1$s\">%1$s</a>.*"; private static final List<String> MODULES_ON_PAGE_IGNORE_LIST = Arrays.asList( "AbstractAccessControlNameCheck.java", "AbstractCheck.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<String> XML_FILESET_LIST = Arrays.asList( "TreeWalker", "name=\"Checker\"", "name=\"Header\"", "name=\"Translation\"", "name=\"SeverityMatchFilter\"", "name=\"SuppressionFilter\"", "name=\"SuppressionCommentFilter\"", "name=\"SuppressWithNearbyCommentFilter\"", "name=\"SuppressWarningsFilter\"", "name=\"BeforeExecutionExclusionFileFilter\"", "name=\"RegexpHeader\"", "name=\"RegexpOnFilename\"", "name=\"RegexpSingleline\"", "name=\"RegexpMultiline\"", "name=\"JavadocPackage\"", "name=\"NewlineAtEndOfFile\"", "name=\"UniqueProperties\"", "name=\"FileLength\"", "name=\"FileTabCharacter\"" ); private static final Set<String> CHECK_PROPERTIES = getProperties(AbstractCheck.class); private static final Set<String> JAVADOC_CHECK_PROPERTIES = getProperties(AbstractJavadocCheck.class); private static final Set<String> FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class); private static final List<String> UNDOCUMENTED_PROPERTIES = Arrays.asList( "Checker.classLoader", "Checker.classloader", "Checker.moduleClassLoader", "Checker.moduleFactory", "TreeWalker.classLoader", "TreeWalker.moduleFactory", "TreeWalker.cacheFile", "TreeWalker.upChild", "SuppressWithNearbyCommentFilter.fileContents", "SuppressionCommentFilter.fileContents" ); private static final Set<String> SUN_MODULES = Collections.unmodifiableSet( new HashSet<>(CheckUtil.getConfigSunStyleModules())); private static final Set<String> GOOGLE_MODULES = Collections.unmodifiableSet( new HashSet<>(CheckUtil.getConfigGoogleStyleModules())); @Test public void testAllChecksPresentOnAvailableChecksPage() throws IOException { final String availableChecks = new String(Files.readAllBytes(AVAILABLE_CHECKS_PATH), UTF_8); Files.walk(JAVA_SOURCES_DIRECTORY).forEach(filePath -> { final String fileName = filePath.getFileName().toString(); if (fileName.matches(CHECK_FILE_NAME) && !MODULES_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 (Path path : XDocUtil.getXdocsFilePaths()) { final String input = new String(Files.readAllBytes(path), UTF_8); final String fileName = path.getFileName().toString(); final Document document = XmlUtil.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) != '>' // no dtd testing yet || unserializedSource.contains("<!")) { continue; } buildAndValidateXml(fileName, unserializedSource); } } } private static void buildAndValidateXml(String fileName, String unserializedSource) throws IOException, ParserConfigurationException, CheckstyleException { // not all examples come with the full xml structure String code = unserializedSource // don't corrupt our own cachefile .replace("target/cachefile", "target/cachefile-test"); if (!hasFileSetClass(code)) { code = "<module name=\"TreeWalker\">\n" + code + "\n</module>"; } if (!code.contains("name=\"Checker\"")) { code = "<module name=\"Checker\">\n" + code + "\n</module>"; } if (!code.startsWith("<?xml")) { final String dtdPath = new File( "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd") .getCanonicalPath(); code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC " + "\"-//Puppy Crawl//DTD Check Configuration 1.3//EN\" \"" + dtdPath + "\">\n" + code; } // validate only XmlUtil.getRawXml(fileName, code, unserializedSource); // can't test ant structure, or old and outdated checks if (!fileName.startsWith("anttask") && !fileName.startsWith("releasenotes")) { validateCheckstyleXml(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 void validateCheckstyleXml(String fileName, String code, String unserializedSource) throws IOException, CheckstyleException { // 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("<suppress ") && !code.contains("<import-control ") && !unserializedSource.startsWith("<property ") && !unserializedSource.startsWith("<taskdef ")) { // validate checkstyle structure and contents try { final Properties properties = new Properties(); properties.setProperty("checkstyle.header.file", new File("config/java.header").getCanonicalPath()); final PropertiesExpander expander = new PropertiesExpander(properties); final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource( new StringReader(code)), expander, false); final Checker checker = new Checker(); try { final ClassLoader moduleClassLoader = Checker.class.getClassLoader(); checker.setModuleClassLoader(moduleClassLoader); checker.configure(config); } finally { checker.destroy(); } } catch (CheckstyleException ex) { throw new CheckstyleException(fileName + " has invalid Checkstyle xml (" + ex.getMessage() + "): " + unserializedSource, ex); } } } @Test public void testAllCheckSections() throws Exception { final ModuleFactory moduleFactory = TestUtils.getPackageObjectFactory(); for (Path path : XDocUtil.getXdocsConfigFilePaths(XDocUtil.getXdocsFilePaths())) { final String fileName = path.getFileName().toString(); if ("config_reporting.xml".equals(fileName)) { continue; } final String input = new String(Files.readAllBytes(path), UTF_8); final Document document = XmlUtil.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; } } } @Test public void testAllCheckSectionsEx() throws Exception { final ModuleFactory moduleFactory = TestUtils.getPackageObjectFactory(); final Path path = Paths.get(XDocUtil.DIRECTORY_PATH + "/config.xml"); final String fileName = path.getFileName().toString(); final String input = new String(Files.readAllBytes(path), UTF_8); final Document document = XmlUtil.getRawXml(fileName, input, input); final NodeList sources = document.getElementsByTagName("section"); for (int position = 0; position < sources.getLength(); position++) { final Node section = sources.item(position); final String sectionName = section.getAttributes().getNamedItem("name") .getNodeValue(); if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) { continue; } validateCheckSection(moduleFactory, fileName, sectionName, section); } } private static void validateCheckSection(ModuleFactory moduleFactory, String fileName, String sectionName, Node section) throws Exception { final Object instance; try { instance = moduleFactory.createModule(sectionName); } catch (CheckstyleException ex) { throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, ex); } int subSectionPos = 0; for (Node subSection : XmlUtil.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; } // optional sections that can be skipped if they have nothing to report if (subSectionPos == 1 && !"Properties".equals(subSectionName)) { validatePropertySection(fileName, sectionName, null, instance); subSectionPos++; } if (subSectionPos == 4 && !"Error Messages".equals(subSectionName)) { validateErrorSection(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: validateErrorSection(fileName, sectionName, subSection, instance); break; case 5: validatePackageSection(fileName, sectionName, subSection, instance); break; case 6: 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 = "Error Messages"; break; case 5: result = "Package"; break; case 6: result = "Parent Module"; break; default: result = null; break; } return result; } private static void validatePropertySection(String fileName, String sectionName, Node subSection, Object instance) throws Exception { final Set<String> properties = getProperties(instance.getClass()); final Class<?> clss = instance.getClass(); fixCapturedProperties(sectionName, instance, clss, properties); if (subSection != null) { Assert.assertTrue(fileName + " section '" + sectionName + "' should have no properties to show", !properties.isEmpty()); validatePropertySectionProperties(fileName, sectionName, subSection, instance, properties); } Assert.assertTrue(fileName + " section '" + sectionName + "' should show properties: " + properties, properties.isEmpty()); } private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss, Set<String> properties) { // remove global properties that don't need documentation if (hasParentModule(sectionName)) { if (AbstractJavadocCheck.class.isAssignableFrom(clss)) { properties.removeAll(JAVADOC_CHECK_PROPERTIES); } else if (AbstractCheck.class.isAssignableFrom(clss)) { properties.removeAll(CHECK_PROPERTIES); } } if (AbstractFileSetCheck.class.isAssignableFrom(clss)) { properties.removeAll(FILESET_PROPERTIES); // override properties.add("fileExtensions"); } // remove undocumented properties new HashSet<>(properties).stream() .filter(p -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + p)) .forEach(properties::remove); if (AbstractCheck.class.isAssignableFrom(clss)) { final AbstractCheck check = (AbstractCheck) instance; final int[] acceptableTokens = check.getAcceptableTokens(); Arrays.sort(acceptableTokens); final int[] defaultTokens = check.getDefaultTokens(); Arrays.sort(defaultTokens); final int[] requiredTokens = check.getRequiredTokens(); Arrays.sort(requiredTokens); if (!Arrays.equals(acceptableTokens, defaultTokens) || !Arrays.equals(acceptableTokens, requiredTokens)) { properties.add("tokens"); } } if (AbstractJavadocCheck.class.isAssignableFrom(clss)) { final AbstractJavadocCheck check = (AbstractJavadocCheck) instance; final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens(); Arrays.sort(acceptableJavadocTokens); final int[] defaultJavadocTokens = check.getDefaultJavadocTokens(); Arrays.sort(defaultJavadocTokens); final int[] requiredJavadocTokens = check.getRequiredJavadocTokens(); Arrays.sort(requiredJavadocTokens); if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens) || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) { properties.add("javadocTokens"); } } } private static void validatePropertySectionProperties(String fileName, String sectionName, Node subSection, Object instance, Set<String> properties) throws Exception { boolean skip = true; boolean didJavadocTokens = false; boolean didTokens = false; for (Node row : XmlUtil.getChildrenElements(XmlUtil.getFirstChildElement(subSection))) { if (skip) { skip = false; continue; } Assert.assertFalse(fileName + " section '" + sectionName + "' should have token properties last", didTokens); final List<Node> columns = new ArrayList<>(XmlUtil.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)) { final AbstractCheck check = (AbstractCheck) instance; validatePropertySectionPropertyTokens(fileName, sectionName, check, columns); didTokens = true; } else if ("javadocTokens".equals(propertyName)) { final AbstractJavadocCheck check = (AbstractJavadocCheck) instance; validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns); didJavadocTokens = true; } else { Assert.assertFalse(fileName + " section '" + sectionName + "' should have javadoc token properties next to last, before tokens", didJavadocTokens); Assert.assertFalse(fileName + " section '" + sectionName + "' should have a description for " + propertyName, columns.get(1) .getTextContent().trim().isEmpty()); final String actualTypeName = columns.get(2).getTextContent().replace("\n", "") .replace("\r", "").replaceAll(" +", " ").trim(); final String actualValue = columns.get(3).getTextContent().replace("\n", "") .replace("\r", "").replaceAll(" +", " ").trim(); Assert.assertFalse(fileName + " section '" + sectionName + "' should have a type for " + propertyName, actualTypeName.isEmpty()); final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance, propertyName); final Class<?> clss = descriptor.getPropertyType(); final String expectedTypeName = getModulePropertyExpectedTypeName(clss, instance, propertyName); final String expectedValue = getModulePropertyExpectedValue(clss, instance, propertyName); if (expectedTypeName != null) { Assert.assertEquals(fileName + " section '" + sectionName + "' should have the type for " + propertyName, expectedTypeName, actualTypeName); if (expectedValue != null) { Assert.assertEquals(fileName + " section '" + sectionName + "' should have the value for " + propertyName, expectedValue, actualValue); } } } } } private static void validatePropertySectionPropertyTokens(String fileName, String sectionName, AbstractCheck check, List<Node> columns) { 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 " + CheckUtil.getTokenText(check.getAcceptableTokens(), check.getRequiredTokens()), columns.get(2).getTextContent() .replaceAll("\\s+", " ").trim()); Assert.assertEquals(fileName + " section '" + sectionName + "' should have all the default tokens", CheckUtil.getTokenText(check.getDefaultTokens(), check.getRequiredTokens()), columns.get(3).getTextContent().replaceAll("\\s+", " ").trim()); } private static void validatePropertySectionPropertyJavadocTokens(String fileName, String sectionName, AbstractJavadocCheck check, List<Node> columns) { Assert.assertEquals(fileName + " section '" + sectionName + "' should have the basic token javadoc description", "javadoc tokens to check", columns.get(1).getTextContent()); Assert.assertEquals( fileName + " section '" + sectionName + "' should have all the acceptable javadoc tokens", "subset of javadoc tokens " + CheckUtil.getJavadocTokenText(check.getAcceptableJavadocTokens(), check.getRequiredJavadocTokens()), columns.get(2).getTextContent() .replaceAll("\\s+", " ").trim()); Assert.assertEquals( fileName + " section '" + sectionName + "' should have all the default javadoc tokens", CheckUtil.getJavadocTokenText(check.getDefaultJavadocTokens(), check.getRequiredJavadocTokens()), columns.get(3).getTextContent() .replaceAll("\\s+", " ").trim()); } /** @noinspection IfStatementWithTooManyBranches */ private static String getModulePropertyExpectedTypeName(Class<?> clss, Object instance, String propertyName) { final String instanceName = instance.getClass().getSimpleName(); String result = null; if (clss == boolean.class) { result = "Boolean"; } else if (clss == int.class) { result = "Integer"; } else if (clss == int[].class) { result = "Integer Set"; } else if (clss == double[].class) { result = "Number Set"; } else if (clss == String[].class) { if (propertyName.endsWith("Tokens") || propertyName.endsWith("Token") || "AtclauseOrderCheck".equals(instanceName) && "target".equals(propertyName) || "MultipleStringLiteralsCheck".equals(instanceName) && "ignoreOccurrenceContext".equals(propertyName)) { result = "subset of tokens TokenTypes"; } else { result = "String Set"; } } else if (clss == URI.class) { result = "URI"; } else if (clss == Pattern.class) { result = "Regular Expression"; } else if (clss == SeverityLevel.class) { result = "Severity"; } else if (clss == Scope.class) { result = "Scope"; } else if (clss != String.class) { Assert.fail("Unknown property type: " + clss.getSimpleName()); } if ("SuppressWarningsHolder".equals(instanceName)) { result = result + " in a format of comma separated attribute=value entries. The " + "attribute is the fully qualified name of the Check and value is its alias."; } return result; } private static String getModulePropertyExpectedValue(Class<?> clss, Object instance, String propertyName) throws Exception { final Field field = getField(instance.getClass(), propertyName); String result = null; if (field != null) { final Object value = field.get(instance); if (clss == boolean.class) { result = value.toString(); } else if (clss == int.class) { if (value.equals(Integer.MAX_VALUE)) { result = "java.lang.Integer.MAX_VALUE"; } else { result = value.toString(); } } else if (clss == int[].class) { result = Arrays.toString((int[]) value).replace("[", "").replace("]", ""); if (result.isEmpty()) { result = "{}"; } } else if (clss == double[].class) { result = Arrays.toString((double[]) value).replace("[", "").replace("]", "") .replace(".0", ""); if (result.isEmpty()) { result = "{}"; } } else if (clss == URI.class) { if (value != null) { result = '"' + value.toString() + '"'; } } else if (clss == Pattern.class) { if (value != null) { result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t") .replace("\r", "\\r").replace("\f", "\\f") + '"'; } if ("\"$^\"".equals(result)) { result += " (empty)"; } } else if (value != null && (clss == SeverityLevel.class || clss == Scope.class)) { result = value.toString().toLowerCase(Locale.ENGLISH); } if (clss != String.class && clss != String[].class && result == null) { result = "null"; } } return result; } private static Field getField(Class<?> clss, String propertyName) { Field result = null; if (clss != null) { try { result = clss.getDeclaredField(propertyName); result.setAccessible(true); } catch (NoSuchFieldException ex) { result = getField(clss.getSuperclass(), propertyName); } } return result; } private static void validateErrorSection(String fileName, String sectionName, Node subSection, Object instance) throws Exception { final Class<?> clss = instance.getClass(); final Set<Field> fields = CheckUtil.getCheckMessages(clss); final Set<String> list = new TreeSet<>(); for (Field field : fields) { // below is required for package/private classes if (!field.isAccessible()) { field.setAccessible(true); } list.add(field.get(null).toString()); } final StringBuilder expectedText = new StringBuilder(); for (String s : list) { expectedText.append(s); expectedText.append("\n"); } if (expectedText.length() > 0) { expectedText.append("All messages can be customized if the default message doesn't " + "suite you.\nPlease see the documentation to learn how to."); } if (subSection == null) { Assert.assertEquals(fileName + " section '" + sectionName + "' should have the expected error keys", "", expectedText.toString()); } else { Assert.assertEquals(fileName + " section '" + sectionName + "' should have the expected error keys", expectedText.toString().trim(), subSection.getTextContent().replaceAll("\n\\s+", "\n").trim()); for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) { final String url = node.getAttributes().getNamedItem("href").getTextContent(); final String linkText = node.getTextContent().trim(); final String expectedUrl; if ("see the documentation".equals(linkText)) { expectedUrl = "config.html#Custom_messages"; } else { expectedUrl = "https://github.com/search?q=" + "path%3Asrc%2Fmain%2Fresources%2F" + clss.getPackage().getName().replace(".", "%2F") + "+filename%3Amessages*.properties+repo%3Acheckstyle%2Fcheckstyle+%22" + linkText + "%22"; } Assert.assertEquals(fileName + " section '" + sectionName + "' should have matching url for '" + linkText + "'", expectedUrl, url); } } } 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()); boolean hasCheckstyle = false; boolean hasGoogle = false; boolean hasSun = false; for (Node node : XmlUtil.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)) { hasCheckstyle = true; expectedUrl = "https://github.com/search?q=" + "path%3Aconfig+filename%3Acheckstyle_checks.xml+" + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName; } else if ("Google Style".equals(linkText)) { hasGoogle = true; expectedUrl = "https://github.com/search?q=" + "path%3Asrc%2Fmain%2Fresources+filename%3Agoogle_checks.xml+" + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName; Assert.assertTrue(fileName + " section '" + sectionName + "' should be in google_checks.xml or not reference 'Google Style'", GOOGLE_MODULES.contains(sectionName)); } else if ("Sun Style".equals(linkText)) { hasSun = true; expectedUrl = "https://github.com/search?q=" + "path%3Asrc%2Fmain%2Fresources+filename%3Asun_checks.xml+" + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName; Assert.assertTrue(fileName + " section '" + sectionName + "' should be in sun_checks.xml or not reference 'Sun Style'", SUN_MODULES.contains(sectionName)); } Assert.assertEquals(fileName + " section '" + sectionName + "' should have matching url", expectedUrl, url); } Assert.assertTrue(fileName + " section '" + sectionName + "' should have a checkstyle section", hasCheckstyle); Assert.assertTrue(fileName + " section '" + sectionName + "' should have a google section since it is in it's config", hasGoogle || !GOOGLE_MODULES.contains(sectionName)); Assert.assertTrue(fileName + " section '" + sectionName + "' should have a sun section since it is in it's config", hasSun || !SUN_MODULES.contains(sectionName)); } 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 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<String> getProperties(Class<?> clss) { final Set<String> result = new TreeSet<>(); final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss); for (PropertyDescriptor p : map) { if (p.getWriteMethod() != null) { result.add(p.getName()); } } return result; } @Test public void testAllStyleRules() throws Exception { for (Path path : XDocUtil.getXdocsStyleFilePaths(XDocUtil.getXdocsFilePaths())) { final String fileName = path.getFileName().toString(); final String input = new String(Files.readAllBytes(path), UTF_8); final Document document = XmlUtil.getRawXml(fileName, input, input); final NodeList sources = document.getElementsByTagName("tr"); Set<String> styleChecks = null; if (path.toFile().getName().contains("google")) { styleChecks = new HashSet<>(GOOGLE_MODULES); } else if (path.toFile().getName().contains("sun")) { styleChecks = new HashSet<>(); } String lastRuleName = null; for (int position = 0; position < sources.getLength(); position++) { final Node row = sources.item(position); final List<Node> columns = new ArrayList<>( XmlUtil.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); } if (!"--".equals(ruleName)) { validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.get(0), "a"), fileName, ruleName); } validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"), XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, fileName, ruleName); lastRuleName = ruleName; } // these modules aren't documented, but are added to the config styleChecks.remove("TreeWalker"); styleChecks.remove("Checker"); Assert.assertTrue(fileName + " requires the following check(s) to appear: " + styleChecks, styleChecks.isEmpty()); } } private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) { Assert.assertEquals(fileName + " rule '" + ruleName + "' must have two row anchors", 2, anchors.size()); final int space = ruleName.indexOf(' '); Assert.assertTrue(fileName + " rule '" + ruleName + "' must have have a space between the rule's number and the rule's name", space != -1); final String ruleNumber = ruleName.substring(0, space); int position = 1; for (Node anchor : anchors) { final String actualUrl; final String expectedUrl; if (position == 1) { actualUrl = anchor.getAttributes().getNamedItem("name").getTextContent(); expectedUrl = ruleNumber; } else { actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent(); expectedUrl = "#" + ruleNumber; } Assert.assertEquals(fileName + " rule '" + ruleName + "' anchor " + position + " shoud have matching name/url", expectedUrl, actualUrl); position++; } } private static void validateStyleModules(Set<Node> checks, Set<Node> configs, Set<String> styleChecks, String fileName, String ruleName) { final Iterator<Node> itrChecks = checks.iterator(); final Iterator<Node> itrConfigs = configs.iterator(); while (itrChecks.hasNext()) { final Node module = itrChecks.next(); final String moduleName = module.getTextContent().trim(); if (!module.getAttributes().getNamedItem("href").getTextContent() .startsWith("config_")) { continue; } Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName + "' shouldn't end with 'Check'", !moduleName.endsWith("Check")); styleChecks.remove(moduleName); for (String configName : new String[] {"config", "test"}) { Node config = null; try { config = itrConfigs.next(); } catch (NoSuchElementException ignore) { Assert.fail(fileName + " rule '" + ruleName + "' module '" + moduleName + "' is missing the config link: " + configName); } Assert.assertEquals(fileName + " rule '" + ruleName + "' module '" + moduleName + "' 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+" + moduleName; Assert.assertEquals(fileName + " rule '" + ruleName + "' module '" + moduleName + "' should have matching " + configName + " url", expectedUrl, configUrl); } else if ("test".equals(configName)) { Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName + "' 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 + "' module '" + moduleName + "' should have matching " + configName + " url", configUrl.endsWith("/" + moduleName + "Test.java")); Assert.assertTrue(fileName + " rule '" + ruleName + "' module '" + moduleName + "' 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()); } }