buildframework/helium/tools/common/java/src/com/nokia/ant/taskdefs/AntLintTask.java
/*
* Copyright (c) 2007-2008 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:
*
*/
package com.nokia.ant.taskdefs;
import java.io.*;
import java.util.*;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.BuildException;
import org.dom4j.io.SAXReader;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.VisitorSupport;
import org.dom4j.Visitor;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.util.regex.*;
/**
* AntLint Task. This task checks for common coding conventions
* and errors in Ant XML script files.
*
* <p>The current checks include:
* <ul>
* <li>Macro names.
* <li>Preset def names.
* <li>Target names.
* <li>Property names and Indentation.
* <li>Project names.</li>
* <li>Project description is present.</li>
* <li>Ant file names.</li>
* <li>runtarget calls a target that has dependencies.</li>
* <li>antcall is used with no param elements and calls a target
* with no dependencies (could use runtarget instead).</li>
* </ul>
* </p>
*
* <p>Checks to be added:
* <ul>
* <li>Help target is defined.</li>
* <li>Optional to thrown warnings about deprecated targets (rename, copydir, copyfile).</li>
* </ul>
* </p>
* @ant.task category="Quality"
*
*/
public class AntLintTask extends Task
{
private ArrayList propertiesVisited = new ArrayList();
private ArrayList antFileSetList = new ArrayList();
private ArrayList<AntFile> antFilelist = new ArrayList<AntFile>();
private String configurationPath;
private boolean tabCharacterCheck;
private boolean propertyNameCheck;
private boolean targetNameCheck;
private boolean indentationCheck;
private boolean presetDefMacroDefNameCheck;
private boolean projectNameCheck;
private boolean descriptionCheck;
private boolean fileNameCheck;
private boolean runTargetCheck;
private boolean antcallCheck;
private String propertyNamePattern;
private String targetNamePattern;
private String presetDefMacroDefNamePattern;
private String projectNamePattern;
private String fileNamePattern;
private AntFile currentFile;
private class AntFile implements Comparable<AntFile>
{
private String name;
private int warningCount;
private int errorCount;
public AntFile(String n) { name = n; }
public void incWarningCount() { warningCount++; }
public int getWarningCount() { return warningCount; }
public void incErrorCount() { errorCount++; }
public int getErrorCount() { return errorCount; }
public String toString()
{
if (errorCount > 0)
throw new BuildException(errorCount + " errors found in " + name);
return warningCount + " warnings " + name;
}
public int compareTo(AntFile o)
{
return new Integer(o.getWarningCount()).compareTo(new Integer(warningCount)) * -1;
}
}
/**
* AntLintTask Constructor
*/
public AntLintTask()
{
setTaskName("antlint");
}
/**
* Add a set of files to copy.
* @param set a set of files to AntLintTask.
* @ant.required
*/
public void addFileset(FileSet set) {
antFileSetList.add(set);
}
/**
* Set the path of the configuration file to use.
*
* @param configurationPath path to config file
* @ant.required
*/
public void setConfigFile(String configurationPath)
{
this.configurationPath = configurationPath;
}
public void checkDuplicateNames(Project project)
{
Hashtable taskdefs = project.getTaskDefinitions();
HashSet<String> classlist = new HashSet<String>();
Enumeration taskdefsenum = taskdefs.keys();
ArrayList<String> macros = new ArrayList<String>();
while (taskdefsenum.hasMoreElements ()) {
String key = (String) taskdefsenum.nextElement();
Class value = (Class) taskdefs.get(key);
macros.add(key);
}
currentFile = new AntFile("General");
antFilelist.add(currentFile);
for (String x : macros)
{
if (macros.contains(x + "Macro") || macros.contains(x + "macro"))
log("W: " + x + " and " + x + "Macro" + " found duplicate name");
currentFile.incWarningCount();
}
}
public final void execute()
{
try
{
Project project = getProject();
getConfiguration();//loads configuration file
checkDuplicateNames(project);
for (Iterator iterator = antFileSetList.iterator(); iterator.hasNext();)
{
FileSet fs = (FileSet) iterator.next();
DirectoryScanner ds = fs.getDirectoryScanner(project);
String[] srcFiles = ds.getIncludedFiles();
String basedir = ds.getBasedir().getPath();
for (int i = 0; i < srcFiles.length; i++)
{
String antFileName = basedir + File.separator + srcFiles[i];
log("*************** Ant File: " + antFileName);
currentFile = new AntFile(antFileName);
antFilelist.add(currentFile);
checkFileName(new File(antFileName).getName());
SAXReader saxReader = new SAXReader();
Document doc = saxReader.read(new File(antFileName));
treeWalk(doc);
SAXParserFactory saxFactory = SAXParserFactory.newInstance();
saxFactory.setNamespaceAware(true);
saxFactory.setValidating(true);
SAXParser parser = saxFactory.newSAXParser();
AntLintHandler handler = new AntLintHandler();
parser.parse(new File(antFileName), handler);
}
Collections.sort(antFilelist);
}
}
catch (Exception e)
{
throw new BuildException("Exception occured while running AntLint task " + e.getMessage());
}
for (AntFile s : antFilelist)
{
log(s.toString());
}
}
public final void treeWalk(final Document document)
{
Element rootElement = document.getRootElement();
Visitor visitorRootElement = new AntProjectVisitor();
rootElement.accept(visitorRootElement);
treeWalk(rootElement);
}
public final void treeWalk(final Element element)
{
for ( int i = 0, size = element.nodeCount(); i < size; i++ )
{
Node node = element.node(i);
if ( node instanceof Element )
{
Visitor visitorElement = new AntXMLVisitor();
node.accept(visitorElement);
treeWalk( (Element) node );
}
}
}
private void checkFileName(String text)
{
try
{
boolean found = false;
Pattern p1 = Pattern.compile(fileNamePattern);
Matcher m1 = p1.matcher(text);
while (m1.find())
{
found = true;
}
if (!found && fileNameCheck) {
log("W: INVALID File Name: " + text);
currentFile.incWarningCount();
}
}
catch (Exception e)
{
throw new BuildException("W: INVALID File Name: " + text + e.getMessage());
}
}
public final void getConfiguration()
{
try
{
SAXReader saxConfigReader = new SAXReader();
Document docConfig = saxConfigReader.read(new File(configurationPath));
Element rootConfig = docConfig.getRootElement();
// iterate through child elements of root
for ( Iterator i = rootConfig.elementIterator(); i.hasNext(); ) {
Element elementConfig = (Element) i.next();
String attrName = elementConfig.attributeValue("name");
if (attrName.equals("TabCharacter")) {
tabCharacterCheck = true;
}
else if (attrName.equals("PropertyName")) {
propertyNameCheck = true;
propertyNamePattern = elementConfig.getText();
}
else if (attrName.equals("TargetName")) {
targetNameCheck = true;
targetNamePattern = elementConfig.getText();
}
else if (attrName.equals("Indentation")) {
indentationCheck = true;
}
else if (attrName.equals("PresetDefMacroDefName")) {
presetDefMacroDefNameCheck = true;
presetDefMacroDefNamePattern = elementConfig.getText();
}
else if (attrName.equals("ProjectName")) {
projectNameCheck = true;
projectNamePattern = elementConfig.getText();
}
else if (attrName.equals("Description")) {
descriptionCheck = true;
}
else if (attrName.equals("FileName")) {
fileNameCheck = true;
fileNamePattern = elementConfig.getText();
}
else if (attrName.equals("RunTarget")) {
runTargetCheck = true;
}
else if (attrName.equals("AntCall")) {
antcallCheck = true;
}
}
}
catch (Exception e)
{
throw new BuildException("Not able to read the ANT lint Configuration file " + configurationPath );
}
}
private class AntXMLVisitor extends VisitorSupport
{
public void visit(Element node)
{
String name = node.getName();
if (name.equals("target"))
{
checkTarget(node);
}
if (name.equals("property"))
{
String text = node.attributeValue("name");
if (text != null && propertyNameCheck)
{
checkPropertyName(text);
}
}
if (name.equals("equals"))
{
String text = node.attributeValue("arg2");
if (text.equals("true") || text.equals("yes"))
{
log("E: " + node.attributeValue("arg1") + " uses 'equals' should use 'istrue' task");
currentFile.incErrorCount();
}
}
if (name.equals("presetdef") || name.equals("macrodef"))
{
String text = node.attributeValue("name");
if (text != null && presetDefMacroDefNameCheck)
{
checkDefName(text);
}
List attributeList = node.elements("attribute");
for (Iterator iterator = attributeList.iterator(); iterator.hasNext();)
{
Element attributeElement = (Element) iterator.next();
String attributeName = attributeElement.attributeValue("name");
checkDefName(attributeName);
}
}
if (name.equals("scriptdef"))
{
String scriptdefname = node.attributeValue("name");
String language = node.attributeValue("language");
checkScriptdef(scriptdefname, node);
if (language.equals("beanshell"))
{
writeBeanshellFile(scriptdefname, node.getText());
}
if (language.equals("jep") || language.equals("jython"))
{
writeJepFile(scriptdefname, node.getText());
checkJepPropertiesInText(node.getText());
}
}
}
private void checkTarget(Element node)
{
String target = node.attributeValue("name");
if (target != null && targetNameCheck)
{
checkTargetName(target);
}
else
{
log("W: Target name not specified!");
currentFile.incWarningCount();
}
checkUseOfIf(node);
checkSizeOfScript(node);
checkTabsInScript(node);
if ((node.elements("runtarget") != null) && runTargetCheck)
{
List runTargetList = node.elements("runtarget");
for (Iterator iterator = runTargetList.iterator(); iterator.hasNext();)
{
Element runTargetElement = (Element) iterator.next();
String runTargetName = runTargetElement.attributeValue("target");
if (checkTargetDependency(runTargetName))
{
log("W: <runtarget> calls the target " + runTargetName + " that has dependencies!");
currentFile.incWarningCount();
}
}
}
if ((node.elements("antcall") != null) && antcallCheck)
{
List antcallList = node.elements("antcall");
for (Iterator iterator = antcallList.iterator(); iterator.hasNext();)
{
Element antcallElement = (Element) iterator.next();
String antcallName = antcallElement.attributeValue("target");
if ((node.elements("param") == null) && !checkTargetDependency(antcallName))
{
log("R: <antcall> is used with no param elements and calls the target " + antcallName + " that has no dependencies! (<runtarget> could be used instead.)");
currentFile.incWarningCount();
}
}
}
List scriptList = node.selectNodes("//target[@name='" + target + "']/descendant::script");
for (Iterator iterator = scriptList.iterator(); iterator.hasNext();)
{
Element scriptElement = (Element) iterator.next();
String language = scriptElement.attributeValue("language");
if (language.equals("jep") || language.equals("jython"))
{
writeJepFile("target_" + target, scriptElement.getText());
checkJepPropertiesInText(scriptElement.getText());
}
}
List scriptList2 = node.selectNodes("//target[@name='" + target + "']/descendant::scriptcondition");
for (Iterator iterator = scriptList2.iterator(); iterator.hasNext();)
{
Element scriptElement2 = (Element) iterator.next();
String language2 = scriptElement2.attributeValue("language");
if (language2.equals("jep") || language2.equals("jython"))
{
writeJepFile("scriptcondition_" + target, scriptElement2.getText());
checkJepPropertiesInText(scriptElement2.getText());
}
}
List pythonList = node.selectNodes("//target[@name='" + target + "']/descendant::*[name()=\"hlm:python\"]");
int i = 0;
for (Iterator iterator = pythonList.iterator(); iterator.hasNext();)
{
Element pythonElement = (Element) iterator.next();
writePythonFile(i + "_" + target, pythonElement.getText());
i++;
}
}
private void writePythonFile(String scriptdefname, String text)
{
try {
String heliumpath = new File(project.getProperty("helium.build.dir")).getCanonicalPath();
new File(heliumpath + File.separator + "python").mkdirs();
File file = new File(heliumpath + File.separator + "python" + File.separator + "target" + scriptdefname + ".py");
PrintWriter output = new PrintWriter(new FileOutputStream(file));
if (!text.equals(""))
{
output.write("def abc():");
for (String t : text.split("\n"))
output.write(" " + t + "\n");
}
output.close();
checkPropertiesInText(text);
} catch (Exception e) {
throw new BuildException("Not able to write python file " + scriptdefname + ".py" );
}
}
private void writeBeanshellFile(String scriptdefname, String text)
{
scriptdefname = "Beanshell" + scriptdefname;
try {
String heliumpath = new File(project.getProperty("helium.build.dir")).getCanonicalPath();
new File(heliumpath + File.separator + "beanshell").mkdirs();
File file = new File(heliumpath + File.separator + "beanshell" + File.separator + scriptdefname + ".java");
PrintWriter output = new PrintWriter(new FileOutputStream(file));
for (String line : text.split("\n"))
{
if (line.trim().startsWith("import"))
output.write(line + "\n");
}
output.write("/**\n * x\n */\npublic final class " + scriptdefname + " {\n");
output.write("private " + scriptdefname + "() { }\n");
output.write("public static void main(String[] args) {\n");
for (String line : text.split("\n"))
{
if (!line.trim().startsWith("import"))
output.write(line + "\n");
}
output.write("} }");
output.close();
} catch (Exception e) {
throw new BuildException("Not able to write Beanshell File " + scriptdefname + ".java" );
}
}
private void writeJepFile(String scriptdefname, String text)
{
if (text.contains("${"))
{
log("E: ${ found in " + scriptdefname);
currentFile.incErrorCount();
}
try {
String heliumpath = new File(project.getProperty("helium.build.dir")).getCanonicalPath();
new File(heliumpath + File.separator + "jep").mkdirs();
File file = new File(heliumpath + File.separator + "jep" + File.separator + scriptdefname + "_jep.py");
PrintWriter output = new PrintWriter(new FileOutputStream(file));
output.write("def abc():\n");
output.write(" attributes = {} # pylint: disable-msg=C0103\n");
output.write(" elements = {} # pylint: disable-msg=C0103\n");
output.write(" project = None # pylint: disable-msg=C0103\n");
output.write(" self = None # pylint: disable-msg=C0103\n");
text = text.replace(" File(", " self.File(");
for (String t : text.split("\n"))
output.write(" " + t + "\n");
output.close();
if (text.contains("import "))
{
File file2 = new File(heliumpath + File.separator + "test_jython.xml");
PrintWriter output2 = new PrintWriter(new FileOutputStream(file2, true));
output2.write("try:\n");
for (String line : text.split("\n"))
{
if (line.trim().startsWith("import ") || line.trim().startsWith("from "))
output2.write(" " + line + "\n");
}
output2.write("except ImportError, e:\n");
output2.write(" print '" + scriptdefname + " failed: ' + str(e)\n");
output2.close();
}
} catch (Exception e) {
throw new BuildException("Not able to write JEP File " + scriptdefname + "_jep.py" );
}
}
private void checkJepPropertiesInText(String text)
{
Pattern p1 = Pattern.compile("getProperty\\([\"']([a-zA-Z0-9\\.]*)[\"']\\)");
Matcher m1 = p1.matcher(text);
ArrayList<String> props = new ArrayList<String>();
while (m1.find())
{
props.add(m1.group(1));
}
for (String group : props)
checkPropertyInModel(group);
}
private void checkPropertiesInText(String text)
{
Pattern p1 = Pattern.compile("r[\"']\\$\\{([a-zA-Z0-9\\.]*)\\}[\"']");
Matcher m1 = p1.matcher(text);
ArrayList<String> props = new ArrayList<String>();
while (m1.find())
{
props.add(m1.group(1));
}
for (String group : props)
checkPropertyInModel(group);
}
public void checkScriptdef(String name, Node node)
{
List<Node> statements = node.selectNodes("//scriptdef[@name='" + name + "']/attribute");
Pattern p1 = Pattern.compile("attributes.get\\([\"']([^\"']*)[\"']\\)");
Matcher m1 = p1.matcher(node.getText());
ArrayList<String> props = new ArrayList<String>();
while (m1.find())
{
props.add(m1.group(1));
}
ArrayList<String> attributes = new ArrayList<String>();
for (Node statement : statements)
{
attributes.add(statement.valueOf("@name"));
}
for (String x : props)
{
if (!attributes.contains(x))
{
log("E: Scriptdef " + name + " does not have attribute " + x);
currentFile.incErrorCount();
}
}
if (!statements.isEmpty() && props.isEmpty())
{
log("W: Scriptdef " + name + " doesn't reference attributes directly, poor style");
currentFile.incWarningCount();
}
else
{
for (Node statement : statements)
{
if (!props.contains(statement.valueOf("@name")))
{
//for (String x : props)
// log(x);
log("E: Scriptdef " + name + " does not use " + statement.valueOf("@name"));
currentFile.incErrorCount();
}
}
}
}
public void checkPropertyInModel(String customerProp)
{
SAXReader xmlReader = new SAXReader();
Document antDoc = null;
try {
File model = new File(project.getProperty("data.model.parsed"));
antDoc = xmlReader.read(model);
} catch (Exception e) {
throw new BuildException("Not able to read data model file " + project.getProperty("data.model.parsed"));
}
List<Node> statements = antDoc.selectNodes("//property");
for (Node statement : statements)
{
if (customerProp.equals(statement.valueOf("name")))
{
return;
}
}
log("W: " + customerProp + " not in data model");
currentFile.incWarningCount();
}
private void checkTargetName(String text)
{
try
{
Pattern p1 = Pattern.compile(targetNamePattern);
Matcher m1 = p1.matcher(text);
if (!m1.matches())
{
log("W: INVALID Target Name: " + text);
currentFile.incWarningCount();
}
}
catch (Exception e)
{
throw new BuildException("Not able to match the target name for " + text);
}
}
private void checkUseOfIf(Element node)
{
String target = node.attributeValue("name");
String targetxpath = "//target[@name='" + target + "']//if";
List<Node> statements2 = node.selectNodes(targetxpath);
for (Node statement : statements2)
{
List conditiontest = statement.selectNodes("./then/property");
if (conditiontest != null && conditiontest.size() == 1)
{
List conditiontest2 = statement.selectNodes("./else/property");
if (conditiontest2 != null && conditiontest2.size() == 1)
{
log("W: Target " + target + " poor use of if-else-property statement, use condition task");
currentFile.incWarningCount();
}
else
{
if (statement.selectNodes("./else").size() == 0)
{
log("W: Target " + target + " poor use of if-then-property statement, use condition task");
currentFile.incWarningCount();
}
}
}
}
List statements = node.selectNodes("//target[@name='" + target + "']/*");
if (!(statements.size() > 1))
{
if (node.selectSingleNode(targetxpath + "/else") == null)
{
if (node.selectSingleNode(targetxpath + "/isset") != null || node.selectSingleNode(targetxpath + "/not/isset") != null)
{
log("W: Target " + target + " poor use of if statement, use <target if|unless=\"prop\"");
//log(node.selectSingleNode(targetxpath).asXML());
currentFile.incWarningCount();
}
}
}
}
private void checkSizeOfScript(Element node)
{
String target = node.attributeValue("name");
List<Node> statements = node.selectNodes("//target[@name='" + target + "']/script | //target[@name='" + target + "']/*[name()=\"hlm:python\"]");
for (Node statement : statements)
{
int size = statement.getText().length();
if (size > 1000)
{
log("W: Target " + target + " has a script with " + size + " characters, code should be inside a python file");
currentFile.incWarningCount();
}
}
}
private void checkTabsInScript(Element node)
{
String target = node.attributeValue("name");
List<Node> statements = node.selectNodes("//target[@name='" + target + "']/script | //target[@name='" + target + "']/*[name()=\"hlm:python\"]");
for (Node statement : statements)
{
if (statement.getText().contains("\t"))
{
log("E: Target " + target + " has a script with tabs");
currentFile.incErrorCount();
}
}
}
private void checkPropertyName(String text)
{
try
{
Pattern p1 = Pattern.compile(propertyNamePattern);
Matcher m1 = p1.matcher(text);
if (!m1.matches() && !propertiesVisited.contains(text))
{
log("W: INVALID Property Name: " + text);
propertiesVisited.add(text);
currentFile.incWarningCount();
}
}
catch (Exception e)
{
throw new BuildException("Not able to match the Property Name for " + text);
}
}
private void checkDefName(String text)
{
try
{
Pattern p1 = Pattern.compile(presetDefMacroDefNamePattern);
Matcher m1 = p1.matcher(text);
if (!m1.matches())
{
log("W: INVALID PRESETDEF/MACRODEF Name: " + text);
currentFile.incWarningCount();
}
}
catch (Exception e)
{
throw new BuildException("Not able to match the MacroDef Name for " + text);
}
}
private boolean checkTargetDependency(String text)
{
boolean dependencyCheck = false;
try
{
Target targetDependency = (Target) project.getTargets().get(text);
if (targetDependency != null)
{
if (targetDependency.getDependencies().hasMoreElements())
{
dependencyCheck = true;
}
}
}
catch (Exception e)
{
throw new BuildException("Not able to get Target Dependency for " + text);
}
finally
{
return dependencyCheck;
}
}
}
private class AntProjectVisitor extends VisitorSupport
{
public void visit(Element node)
{
String name = node.getName();
if (name.equals("project"))
{
String text = node.attributeValue("name");
if (text != null && projectNameCheck)
{
checkProjectName(text);
}
else
{
log("W: Project name not specified!");
currentFile.incWarningCount();
}
if ((node.element("description") == null) && descriptionCheck)
{
log("W: Description not specified!");
currentFile.incWarningCount();
}
}
}
private void checkProjectName(String text)
{
try
{
Pattern p1 = Pattern.compile(projectNamePattern);
Matcher m1 = p1.matcher(text);
if (!m1.matches())
{
log("W: INVALID Project Name: " + text);
currentFile.incWarningCount();
}
}
catch (Exception e)
{
throw new BuildException("Not able to match Project Name for " + text);
}
}
}
private class AntLintHandler extends DefaultHandler
{
private int indentLevel;
private int indentSpace;
private Locator locator;
private boolean textElement;
private int currentLine;
private StringBuffer strBuff = new StringBuffer();
public AntLintHandler()
{
super();
}
public void setDocumentLocator(Locator locator)
{
this.locator = locator;
}
public void startDocument ()
{
indentLevel -= 4;
}
public void endDocument ()
{
}
public void startElement(String uri, String name,
String qName, Attributes atts)
{
countSpaces();
indentLevel += 4; //When an element start tag is encountered, indentLevel is increased 4 spaces.
checkIndent();
currentLine = locator.getLineNumber();
}
public void endElement(String uri, String name, String qName)
{
countSpaces();
//Ignore end tags in the same line
if (currentLine != locator.getLineNumber()) {
checkIndent();
}
indentLevel -= 4; //When an element end tag is encountered, indentLevel is decreased 4 spaces.
textElement = false;
}
private void checkIndent()
{
if (indentationCheck)
{
if ((indentSpace != indentLevel) && !textElement)
{
log("E:" + locator.getLineNumber() + ": Bad indentation!");
currentFile.incErrorCount();
}
}
}
public void characters(char[] ch, int start, int length)
{
for (int i = start; i < start + length; i++)
{
strBuff.append(ch[i]);
}
}
public void countSpaces()
{
//Counts spaces and tabs in every newline.
int numSpaces = 0;
for (int i = 0; i < strBuff.length(); i++)
{
switch (strBuff.charAt(i)) {
case '\t':
numSpaces += 4;
if (tabCharacterCheck)
{
log("E:" + locator.getLineNumber() + ": Tabs should not be used!");
currentFile.incErrorCount();
}
break;
case '\n':
numSpaces = 0;
break;
case '\r':
break;
case ' ':
numSpaces++;
break;
default:
textElement = true;
break;
}
}
indentSpace = numSpaces;
strBuff.delete(0,strBuff.length());
}
}
}