Created
December 28, 2017 17:43
-
-
Save jsteinshouer/8a21d1445a4f24be050946bb85c86136 to your computer and use it in GitHub Desktop.
Run CFLint with CommandBox Task Runner - SVN
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* 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"; | |
.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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--- | |
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">×</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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--- | |
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--- | |
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