////////////////////////////////////////////////////////////////////////////////
// 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.github.sevntu.checkstyle.internal;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.Collections;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.beanutils.PropertyUtils;
import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
public final class ChecksTest {
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);
@Test
public void verifyTestConfigurationFiles() throws Exception {
final Set<Class<?>> modules = CheckUtil.getCheckstyleModules();
final Set<String> packages = CheckUtil.getPackages(modules);
Assert.assertTrue("no modules", modules.size() > 0);
// sonar
final File extensionFile = new File(getSonarPath("checkstyle-extensions.xml"));
Assert.assertTrue("'checkstyle-extensions.xml' must exist in sonar",
extensionFile.exists());
final String input = new String(Files.readAllBytes(extensionFile.toPath()), UTF_8);
final Document document = XmlUtil.getRawXml(extensionFile.getAbsolutePath(), input,
input);
validateSonarFile(document, new HashSet<>(modules));
// eclipsecs
for (String p : packages) {
Assert.assertTrue("folder " + p + " must exist in eclipsecs", new File(
getEclipseCsPath(p)).exists());
final Set<Class<?>> packgeModules = CheckUtil.getModulesInPackage(modules, p);
validateEclipseCsMetaXmlFile(
new File(getEclipseCsPath(p + "/checkstyle-metadata.xml")), p, new HashSet<>(
packgeModules));
validateEclipseCsMetaPropFile(new File(getEclipseCsPath(p
+ "/checkstyle-metadata.properties")), p, new HashSet<>(packgeModules));
}
}
private static void validateSonarFile(Document document, Set<Class<?>> modules) {
final NodeList rules = document.getElementsByTagName("rule");
for (int position = 0; position < rules.getLength(); position++) {
final Node rule = rules.item(position);
final Set<Node> children = XmlUtil.getChildrenElements(rule);
final String key = XmlUtil.findElementByTag(children, "key").getTextContent();
final Class<?> module = findModule(modules, key);
modules.remove(module);
Assert.assertNotNull("Unknown class found in sonar: " + key, module);
final String moduleName = module.getName();
final Node name = XmlUtil.findElementByTag(children, "name");
Assert.assertNotNull(moduleName + " requires a name in sonar", name);
Assert.assertFalse(moduleName + " requires a name in sonar", name.getTextContent()
.isEmpty());
final Node categoryNode = XmlUtil.findElementByTag(children, "category");
String expectedCategory = module.getCanonicalName();
final int lastIndex = expectedCategory.lastIndexOf('.');
expectedCategory = expectedCategory.substring(
expectedCategory.lastIndexOf('.', lastIndex - 1) + 1, lastIndex);
Assert.assertNotNull(moduleName + " requires a category in sonar", categoryNode);
final Node categoryAttribute = categoryNode.getAttributes().getNamedItem("name");
Assert.assertNotNull(moduleName + " requires a category name in sonar",
categoryAttribute);
Assert.assertEquals(moduleName + " requires a valid category in sonar",
expectedCategory, categoryAttribute.getTextContent());
final Node description = XmlUtil.findElementByTag(children, "description");
Assert.assertNotNull(moduleName + " requires a description in sonar", description);
final Node configKey = XmlUtil.findElementByTag(children, "configKey");
final String expectedConfigKey = "Checker/TreeWalker/" + key;
Assert.assertNotNull(moduleName + " requires a configKey in sonar", configKey);
Assert.assertEquals(moduleName + " requires a valid configKey in sonar",
expectedConfigKey, configKey.getTextContent());
validateSonarProperties(module, XmlUtil.findElementsByTag(children, "param"));
}
for (Class<?> module : modules) {
Assert.fail("Module not found in sonar: " + module.getCanonicalName());
}
}
private static void validateSonarProperties(Class<?> module, Set<Node> parameters) {
final String moduleName = module.getName();
final Set<String> properties = getFinalProperties(module);
for (Node parameter : parameters) {
final NamedNodeMap attributes = parameter.getAttributes();
final Node paramKeyNode = attributes.getNamedItem("key");
Assert.assertNotNull(moduleName + " requires a key for unknown parameter in sonar",
paramKeyNode);
final String paramKey = paramKeyNode.getTextContent();
Assert.assertFalse(moduleName
+ " requires a valid key for unknown parameter in sonar",
paramKey.isEmpty());
Assert.assertTrue(moduleName + " has an unknown parameter in sonar: " + paramKey,
properties.remove(paramKey));
}
for (String property : properties) {
Assert.fail(moduleName + " parameter not found in sonar: " + property);
}
}
private static void validateEclipseCsMetaXmlFile(File file, String packge,
Set<Class<?>> packgeModules) throws Exception {
Assert.assertTrue("'checkstyle-metadata.xml' must exist in eclipsecs in inside " + packge,
file.exists());
final String input = new String(Files.readAllBytes(file.toPath()), UTF_8);
final Document document = XmlUtil.getRawXml(file.getAbsolutePath(), input, input);
final NodeList ruleGroups = document.getElementsByTagName("rule-group-metadata");
Assert.assertTrue(packge + " checkstyle-metadata.xml must contain only one rule group",
ruleGroups.getLength() == 1);
for (int position = 0; position < ruleGroups.getLength(); position++) {
final Node ruleGroup = ruleGroups.item(position);
final Set<Node> children = XmlUtil.getChildrenElements(ruleGroup);
validateEclipseCsMetaXmlFileRules(packge, packgeModules, children);
}
for (Class<?> module : packgeModules) {
Assert.fail("Module not found in " + packge + " checkstyle-metadata.xml: "
+ module.getCanonicalName());
}
}
private static void validateEclipseCsMetaXmlFileRules(String packge,
Set<Class<?>> packgeModules, Set<Node> rules) throws Exception {
for (Node rule : rules) {
final NamedNodeMap attributes = rule.getAttributes();
final Node internalNameNode = attributes.getNamedItem("internal-name");
Assert.assertNotNull(packge + " checkstyle-metadata.xml must contain an internal name",
internalNameNode);
final String internalName = internalNameNode.getTextContent();
final String classpath = "com.github.sevntu.checkstyle.checks." + packge + "."
+ internalName;
final Class<?> module = findModule(packgeModules, classpath);
packgeModules.remove(module);
Assert.assertNotNull("Unknown class found in " + packge + " checkstyle-metadata.xml: "
+ internalName, module);
final Node nameAttribute = attributes.getNamedItem("name");
Assert.assertNotNull(packge + " checkstyle-metadata.xml requires a name for "
+ internalName, nameAttribute);
Assert.assertEquals(packge + " checkstyle-metadata.xml requires a valid name for "
+ internalName, "%" + internalName + ".name", nameAttribute.getTextContent());
final Node parentAttribute = attributes.getNamedItem("parent");
Assert.assertNotNull(packge + " checkstyle-metadata.xml requires a parent for "
+ internalName, parentAttribute);
Assert.assertEquals(packge + " checkstyle-metadata.xml requires a valid parent for "
+ internalName, "TreeWalker", parentAttribute.getTextContent());
final Set<Node> children = XmlUtil.getChildrenElements(rule);
validateEclipseCsMetaXmlFileRule(packge, module, children);
}
}
private static void validateEclipseCsMetaXmlFileRule(String packge, Class<?> module,
Set<Node> children) throws Exception {
final String moduleName = module.getSimpleName();
final Set<String> properties = getFinalProperties(module);
final Set<Field> fieldMessages = CheckUtil.getCheckMessages(module);
final Set<String> messages = new TreeSet<>();
for (Field fieldMessage : fieldMessages) {
// below is required for package/private classes
if (!fieldMessage.isAccessible()) {
fieldMessage.setAccessible(true);
}
messages.add(fieldMessage.get(null).toString());
}
for (Node child : children) {
final NamedNodeMap attributes = child.getAttributes();
switch (child.getNodeName()) {
case "alternative-name":
final Node internalNameNode = attributes.getNamedItem("internal-name");
Assert.assertNotNull(packge
+ " checkstyle-metadata.xml must contain an internal name for "
+ moduleName, internalNameNode);
final String internalName = internalNameNode.getTextContent();
Assert.assertEquals(packge
+ " checkstyle-metadata.xml requires a valid internal-name for "
+ moduleName, module.getName(), internalName);
break;
case "description":
Assert.assertEquals(
packge + " checkstyle-metadata.xml requires a valid description for "
+ moduleName, "%" + moduleName + ".desc", child.getTextContent());
break;
case "property-metadata":
final String propertyName = attributes.getNamedItem("name").getTextContent();
Assert.assertTrue(packge + " checkstyle-metadata.xml has an unknown parameter for "
+ moduleName + ": " + propertyName, properties.remove(propertyName));
final Node firstChild = child.getFirstChild().getNextSibling();
Assert.assertNotNull(packge
+ " checkstyle-metadata.xml requires atleast one child for " + moduleName
+ ", " + propertyName, firstChild);
Assert.assertEquals(
packge
+ " checkstyle-metadata.xml should have a description for the first child of "
+ moduleName + ", " + propertyName, "description",
firstChild.getNodeName());
Assert.assertEquals(packge
+ " checkstyle-metadata.xml requires a valid description for " + moduleName
+ ", " + propertyName, "%" + moduleName + "." + propertyName,
firstChild.getTextContent());
break;
case "message-key":
final String key = attributes.getNamedItem("key").getTextContent();
Assert.assertTrue(packge + " checkstyle-metadata.xml has an unknown message for "
+ moduleName + ": " + key, messages.remove(key));
break;
default:
Assert.fail(packge + " checkstyle-metadata.xml unknown node for " + moduleName
+ ": " + child.getNodeName());
break;
}
}
for (String property : properties) {
Assert.fail(packge + " checkstyle-metadata.xml missing parameter for " + moduleName
+ ": " + property);
}
for (String message : messages) {
Assert.fail(packge + " checkstyle-metadata.xml missing message for " + moduleName
+ ": " + message);
}
}
private static void validateEclipseCsMetaPropFile(File file, String packge,
Set<Class<?>> packgeModules) throws Exception {
Assert.assertTrue("'checkstyle-metadata.properties' must exist in eclipsecs in inside "
+ packge, file.exists());
final Properties prop = new Properties();
prop.load(new FileInputStream(file));
final Set<Object> properties = new HashSet<>(Collections.list(prop.keys()));
for (Class<?> module : packgeModules) {
final String moduleName = module.getSimpleName();
Assert.assertTrue(moduleName + " requires a name in eclipsecs properties " + packge,
properties.remove(moduleName + ".name"));
Assert.assertTrue(moduleName + " requires a desc in eclipsecs properties " + packge,
properties.remove(moduleName + ".desc"));
final Set<String> moduleProperties = getFinalProperties(module);
for (String moduleProperty : moduleProperties) {
Assert.assertTrue(moduleName + " requires the property " + moduleProperty
+ " in eclipsecs properties " + packge,
properties.remove(moduleName + "." + moduleProperty));
}
}
for (Object property : properties) {
Assert.fail("Unknown property found in eclipsecs properties " + packge + ": "
+ property);
}
}
private static Class<?> findModule(Set<Class<?>> modules, String classPath) {
Class<?> result = null;
for (Class<?> module : modules) {
if (module.getCanonicalName().equals(classPath)) {
result = module;
break;
}
}
return result;
}
private static Set<String> getFinalProperties(Class<?> clss) {
final Set<String> properties = getProperties(clss);
if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
properties.removeAll(JAVADOC_CHECK_PROPERTIES);
}
else if (AbstractCheck.class.isAssignableFrom(clss)) {
properties.removeAll(CHECK_PROPERTIES);
}
else if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
properties.removeAll(FILESET_PROPERTIES);
// override
properties.add("fileExtensions");
}
return properties;
}
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;
}
/**
* Returns canonical path for the file with the given file name.
*
* @param filename file name.
* @return canonical path for the file name.
* @throws IOException if I/O exception occurs while forming the path.
*/
private static String getEclipseCsPath(String filename) throws IOException {
return new File("../eclipsecs-sevntu-plugin/src/com/github/sevntu/checkstyle/checks/"
+ filename).getCanonicalPath();
}
/**
* Returns canonical path for the file with the given file name.
*
* @param filename file name.
* @return canonical path for the file name.
* @throws IOException if I/O exception occurs while forming the path.
*/
private static String getSonarPath(String filename) throws IOException {
return new File(
"../sevntu-checkstyle-sonar-plugin/src/main/resources/com/github/sevntu/"
+ "checkstyle/sonar/" + filename).getCanonicalPath();
}
}