Skip to content

Instantly share code, notes, and snippets.

@jsteinshouer
Created December 28, 2017 17:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsteinshouer/8a21d1445a4f24be050946bb85c86136 to your computer and use it in GitHub Desktop.
Save jsteinshouer/8a21d1445a4f24be050946bb85c86136 to your computer and use it in GitHub Desktop.
Run CFLint with CommandBox Task Runner - SVN
/**
*
* CommandBox Task for running cflint
*
* build/tasks/CFLint.cfc
*
*/
component output="false" accessors="true" {
property name="currentBranch";
/* mixins */
include "helpers/xmlToStruct.cfm";
include "helpers/svn.cfm";
/*
* Constructor
*/
function init() {
rootPath = fileSystemUtil.resolvePath( '.' );
cflintJAR = rootPath & "build/bin/cflint-1.2.3-all/cflint-1.2.3-all.jar";
// configFile = rootPath & "build/resources/cflint/cflint.json";
configFile = "build/resources/cflint/cflint.json";
reportTemplate = "../resources/cflint/report.cfm";
htmlResultFile = rootPath & "build\artifacts\cflint-results.html";
}
public function run( any files="", branch = false, html = false) {
checkIfInstalled();
arguments.files = listToArray( arguments.files );
if ( files.len() ) {
var files = arguments.files();
}
else if (arguments.branch) {
var files = svnLog();
}
else {
var files = svnStatus();
}
// Remove path from files to shorten the command string. Limit is 8191 characters on windows
var files = files.map( function(item) {
return replace(item, rootPath, "");
});
runReport( local.files, arguments.html );
}
public function diff( html = false) {
checkIfInstalled();
var files = svnDiff();
runReport( files, arguments.html );
}
private void function runReport( required files , html = false ) {
var reportData = getReportData( files );
if ( html ) {
htmlReport( reportData );
print.greenLine("Report generated at #htmlResultFile#");
print.greenLine("Opening browser...");
var runtime = createObject( "java", "java.lang.Runtime" ).getRuntime();
runtime.exec( [ "rundll32", "url.dll,FileProtocolHandler", "file:///#rootPath#/build/artifacts/cflint-results.html" ] );
}
else {
displayReport( reportData );
}
/* Make the task fail if an error exists */
if ( reportData.errorExists ) {
/* Flush any output to the console */
print.line().toConsole();
error("Please fix errors found by CFLint!");
}
}
private struct function getReportData( required array files ) {
var data = {
"version" = "1.2.3",
"timestamp" = now(),
"files" = {},
"errorExists" = false
};
var cflintResults = runCFLint( arguments.files );
data.counts = cflintResults.counts;
for (var issue in cflintResults.issues) {
for ( var item in issue.locations ) {
if ( !structKeyExists( data.files, item.file ) ) {
data.files[ item.file ] = [];
}
var newIssue = {
severity = issue.severity,
id = issue.id,
message = item.message,
line = item.line,
column = item.column,
expression = item.expression
};
switch ( issue.severity ) {
case "ERROR":
newIssue.color = "red";
data.errorExists = true;
break;
case "WARNING":
newIssue.color = "yellow";
break;
default:
newIssue.color = "magenta";
}
data.files[ item.file ].append( newIssue );
}
}
return data;
}
private struct function runCFLint( required array files ) {
var outputFile = "#rootPath#/build/tmp/cflint-out.json";
print
.line("Running cflint for file:")
.text(files.toList( chr(10) ) )
.line()
.toConsole();
command("!java -jar ""#cflintJAR#"" -file ""#files.toList()#"" -json -jsonfile ""#outputFile#"" -configfile ""#configFile#""")
.inWorkingDirectory( rootPath )
.run( returnOutput=true );
var output = fileRead( outputFile );
fileDelete( outputFile );
return deserializeJSON( output );
}
private string function htmlReport( required data ) {
var content = "";
savecontent variable="content" {
include reportTemplate;
}
if ( fileExists( htmlResultFile ) ) {
fileDelete( htmlResultFile );
}
fileWrite( htmlResultFile, content );
// command("browse ""file:///#htmlFile#""").run();
}
private void function displayReport( required data ) {
displaySummary( data );
print.line();
for ( var file in data.files ) {
print.greenLine( chr(9) & file & " " & data.files[ file ].len() );
for (var issue in data.files[ file ]) {
//print.text(getInstance("Formatter").formatJson(issue));
print.text( repeatString( chr(9),2 ) );
print.text(issue.severity, issue.color);
print.text( ": ");
print.boldText(issue.id);
print.text(", #issue.message# ");
print.cyanLine("[#issue.line#,#issue.column#]");
}
}
}
private void function displaySummary( required data ) {
print.line();
print.greenLine( chr(9) & "Total Files:" & chr(9) & data.counts.totalFiles );
print.greenLine( chr(9) & "Total Lines:" & chr(9) & data.counts.totalLines );
for (var item in data.counts.countBySeverity ) {
print.text(chr(9));
switch (item.severity) {
case "ERROR":
print.boldRedText("ERRORS:" & chr(9) & chr(9));
break;
case "WARNING":
print.boldYellowText( "WARNINGS:" & chr(9) );
break;
default:
print.boldMagentaText( item.severity & ":" & chr(9) & chr(9) );
}
print.line( item.count );
}
}
/*
* Install CFLint if not already installed
*/
private void function checkIfInstalled() {
if ( !fileExists(cflintJAR) ) {
command("install")
.inWorkingDirectory( rootPath )
.params(
id = "jar:https://github.com/cflint/CFLint/releases/download/CFLint-1.2.3/CFLint-1.2.3-all.jar",
directory = "./build/bin"
)
.run();
}
}
}
<!---
Template for HTML report
build/resources/cflint/report.cfm
--->
<!doctype html>
<html lang="en">
<head>
<title>CFLint Results</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic.min.css" crossorigin="anonymous">
</head>
<body>
<div class="container" style="margin-top: 30px; margin-bottom: 40px">
<h2>CFLint Results</h2>
<cfoutput>
<table class="table table-bordered table-sm">
<tbody>
<tr>
<th>Version</th>
<td>#data.version#</td>
</tr>
<tr>
<th>Timestamp</th>
<td>#dateTimeFormat(data.timestamp)#</td>
</tr>
<tr>
<th>Files</th>
<td>#data.counts.totalFiles#</td>
</tr>
<tr>
<th>Lines</th>
<td>#data.counts.totalLines#</td>
</tr>
<cfloop array="#data.counts.countBySeverity#" index="item">
<tr>
<th>
<cfswitch expression="#item.severity#">
<cfcase value="ERROR"><span class="oi" data-glyph="bug"></span></cfcase>
<cfcase value="WARNING"><span class="oi" data-glyph="warning"></span></cfcase>
<cfdefaultcase><span class="oi" data-glyph="info"></span></cfdefaultcase>
</cfswitch>
#item.severity#
</th>
<td>#item.count#</td>
</tr>
</cfloop>
</tbody>
</table>
<div id="accordion" role="tablist">
<cfset index = 1>
<cfloop collection="#data.files#" key="file">
<div class="card">
<div class="card-header" role="tab" id="heading#index#">
<h5 class="mb-0">
<a data-toggle="collapse" class="collapsed" href="##collapse#index#" aria-expanded="true" aria-controls="collapse#index#">
#file#
</a>
</h5>
</div>
<div id="collapse#index#" class="collapse hide" role="tabpanel" aria-labelledby="heading#index#" data-parent="##accordion">
<div class="card-body">
<table class="table">
<tbody>
<cfloop array="#data.files[file]#" index="issue">
<tr>
<td>
<cfswitch expression="#issue.severity#">
<cfcase value="ERROR"><span class="oi" data-glyph="bug"></span></cfcase>
<cfcase value="WARNING"><span class="oi" data-glyph="warning"></span></cfcase>
<cfdefaultcase><span class="oi" data-glyph="info"></span></cfdefaultcase>
</cfswitch>
</td>
<td>#issue.id#</td>
<td>#issue.message#</td>
<!--- <td>[#issue.line#,#issue.column#]</td> --->
<td>
<button type="button" class="btn btn-secondary btn-sm" data-toggle="modal" data-target="##expressionModal" data-issue="#encodeForHTMLAttribute(serializeJSON({'id' = issue.id, 'message' = issue.message, 'line' = issue.line}))#" data-file="#listLast(file,"\")#" data-expression="#encodeForHTML(issue.expression)#">
[#issue.line#,#issue.column#]
</button>
</td>
</tr>
</cfloop>
</tbody>
</table>
</div>
</div>
</div>
<cfset index++>
</cfloop>
</div>
</div>
</cfoutput>
<div class="modal fade" id="expressionModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h6 class="issue-id"></h6>
<p class="issue-msg"></p>
<p class="issue-line"></p>
<pre></pre>
</div>
</div>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script>
$(function () {
//$('[data-toggle="popover"]').popover({html: true});
$('#expressionModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var expression = button.data('expression');
var issue = button.data('issue');
var file = button.data('file');
var modal = $(this);
modal.find('pre').text( expression );
modal.find('.issue-id').text( issue.id );
modal.find('.issue-msg').text( issue.message );
modal.find('.issue-line').text( "Line: " + issue.line );
modal.find('.modal-title').text( file );
})
});
</script>
</body>
</html>
<!---
UDF Library used to execute SVN commands
build/tasks/helper/svn.cfm
--->
<cfscript>
/*
* Gets files changed in working copy
*/
private function svnStatus() {
var output = cmd("svn status");
var files = [];
for ( var line in listToArray( output, chr(10) ) ) {
var change = listToArray( line, " " );
var file = change[2];
if ( listLast( file, ".") == "cfm" || listLast( file, ".") == "cfc") {
files.append( file );
}
/* Need to get files from new directories since SVN does not report them */
else if (change[1] == "?" && directoryExists( rootPath & "\" & change[2])) {
var newFiles = directoryList(
filter = "*.cfm|*.cfc",
listInfo = 'path',
recurse = true,
path = rootPath & "\" & change[2]
);
files.append( newFiles, true );
}
}
//print.text(getInstance("Formatter").formatJSON(files));
return files;
}
/*
* Get the files that have changed between local development branch and trunk
*/
private function svnDiff() {
var files = [];
var info = svnInfo();
var currentBranch = info["URL"];
var trunk = info["Repository Root"] & "/trunk";
var commandString = "svn diff ""#currentBranch#"" ""#trunk#"" --summarize";
var output = cmd( commandString );
for ( var line in listToArray( output, chr(10) ) ) {
var data = listToArray(line," ",false,true);
//print.text(getInstance("Formatter").formatJSON(data)).toConsole();
var file = data[2];
if ( listLast( file, ".") == "cfm" || listLast( file, ".") == "cfc") {
files.append( replace(file, currentBranch, rootPath) );
}
}
}
/*
* Gets files changed in the current development branch
*/
private function svnLog() {
var files = [];
var info = svnInfo();
setCurrentBranch( replace( info["Relative URL"], "^", "" ) );
var commandString = "svn log --verbose --stop-on-copy --xml";
var output = cmd( commandString );
var log = xmlToStruct( xmlParse( output ) ).log[1];
//print.text( getInstance("Formatter").formatJSON(log) );
for (var item in log) {
for (var logItem in log[item]) {
if ( isStruct(logItem.paths.path) ) {
addSVNFile(files, logItem.paths.path);
}
else {
for (var path in logItem.paths.path) {
addSVNFile(files, path);
}
}
}
}
return files;
}
/*
* Add an svn file to the collection
*/
private void function addSVNFile( required array files, required struct svnPath ) {
var path = replace(svnPath.value, currentBranch, rootPath)
if (
svnPath.attributes.kind == "file"
&& ( right(path, 4) == ".cfc" || right(path, 4) == ".cfm" )
&& svnPath.attributes.action != "D"
&& !files.find( path )
) {
files.append( path );
}
}
/* Get SVN information for current working directory */
private struct function svnInfo() {
/* Update first to makes sure we have the most recent info */
cmd("svn update");
var output = cmd("svn info");
var info = {};
for ( var line in listToArray( output, chr(10) ) ) {
var data = listToArray(line,": ",false,true);
info[ data[1] ] = data[2];
}
return info;
}
/*
* Executes external OS process and returns output. Current run command does not return output for external processes.
*/
private string function cmd(required command){
var commandArray = [ 'cmd','/a','/c', arguments.command ];
try{
// grab the current working directory
var CWDFile = createObject( 'java', 'java.io.File' ).init( fileSystemUtil.resolvePath( '' ) );
var process = createObject( "java", "java.lang.ProcessBuilder" )
.init( commandArray )
// Sets current working directory for the process
.directory( CWDFile )
// Fires process async
.start();
// waits for it to exit, returning the exit code
var exitCode = process.waitFor();
// Get the input
var i = createObject("java","java.io.InputStreamReader").init(process.getInputStream());
var br = createObject("java","java.io.BufferedReader").init(i);
var output = "";
var line = br.readLine();
if (!isNull(line)) {
while ( !isNull(line) ) {
output = output & line & chr(10);
line = br.readLine();
}
}
br.close();
i.close();
if( exitCode != 0 ) {
error( 'Command returned failing exit code [#exitCode#]' );
}
else {
return output;
}
} catch( any e ){
error( '#e.message##CR##e.detail#' );
}
}
</cfscript>
<!---
UDF to convert an xml document to a structure
build/tasks/helper/xmlToStruct.cfm
--->
<cfscript>
private struct function xmlToStruct( xml x ) {
var s = {};
if(xmlGetNodeType(x) == "DOCUMENT_NODE") {
s[structKeyList(x)] = xmlToStruct(x[structKeyList(x)]);
}
if( structKeyExists(x, "xmlAttributes") && !structIsEmpty(x.xmlAttributes) ) {
s.attributes = {};
for(var item in x.xmlAttributes) {
s.attributes[item] = x.xmlAttributes[item];
}
}
if(structKeyExists(x, "xmlText") && len(trim(x.xmlText))) {
s.value = x.xmlText;
}
if( structKeyExists(x, "xmlChildren") ) {
for(var i=1; i <= arrayLen(x.xmlChildren); i++) {
if(structKeyExists(s, x.xmlchildren[i].xmlname)) {
if(!isArray(s[x.xmlChildren[i].xmlname])) {
var temp = s[x.xmlchildren[i].xmlname];
s[x.xmlchildren[i].xmlname] = [temp];
}
arrayAppend(s[x.xmlchildren[i].xmlname], xmlToStruct(x.xmlChildren[i]));
} else {
s[x.xmlChildren[i].xmlName] = xmlToStruct(x.xmlChildren[i]);
}
}
}
return s;
}
</cfscript>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment