org.codenarc.CodeNarc.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of CodeNarc Show documentation
Show all versions of CodeNarc Show documentation
The CodeNarc project provides a static analysis tool for Groovy code.
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codenarc
import org.codenarc.analyzer.FilesystemSourceAnalyzer
import org.codenarc.analyzer.FilesSourceAnalyzer
import org.codenarc.analyzer.SourceAnalyzer
import org.codenarc.plugin.baseline.BaselineResultsPlugin
import org.codenarc.report.JsonReportWriter
import org.codenarc.report.HtmlReportWriter
import org.codenarc.report.ReportWriterFactory
import org.codenarc.results.Results
import org.codenarc.util.CodeNarcVersion
import org.codenarc.util.io.DefaultResourceFactory
import org.codenarc.util.io.ResourceFactory
/**
* Command-line runner for CodeNarc.
*
* The supported command-line parameters are all of the form: "-OPTION=VALUE", where OPTION is one
* of the options in the following list.
*
* - help - Display the command-line help; If present, this must be the only command-line parameter.
* - basedir - The base directory for the source code to be analyzed. This is the root of the
* directory tree. Defaults to the current directory ('.').
* - sourcefiles - The comma-separated list of files we want to analyze. If set, -includes and -exclude arguments are ignored.
* - rulesetfiles - The path to the Groovy or XML RuleSet definition files, relative to the classpath. This can be a
* single file path, or multiple paths separated by commas. Each path may be optionally prefixed by
* any of the valid java.net.URL prefixes, such as "file:" (to load from a relative or absolute filesystem path),
* or "http:". If it is a URL, its path may be optionally URL-encoded. That can be useful if the path contains
* any problematic characters, such as comma (',') or hash ('#'). See URLEncoder#encode(java.lang.String, java.lang.String).
* Defaults to "rulesets/basic.xml".
* - ruleset - JSON string (URL-encoded in UTF-8) containing a ruleSet in JSON format (if set, rulesetfiles will be ignored).
*
- excludeBaseline - The filename of the optional baseline. If not set, no baseline will be used.
*
- includes - The comma-separated list of Ant file patterns specifying files that must be included;
* all files are included when omitted.
* - excludes - The comma-separated list of Ant file patterns specifying files that must be excluded;
* no files are excluded when omitted.
* - maxPriority1Violations - The maximum number of priority 1 violations allowed. Optional.
* - maxPriority2Violations - The maximum number of priority 2 violations allowed. Optional.
* - maxPriority3Violations - The maximum number of priority 3 violations allowed. Optional.
* - failOnError - whether to terminate and fail the task if any errors occur parsing source files. Optional.
* - title - The title description for this analysis; used in the output report(s), if supported. Optional.
* - report - The definition of the report to produce. The option value is of the form TYPE[:FILENAME|:stdout].
* where TYPE is 'html' and FILENAME is the filename (with optional path) of the output report filename.
* If the TYPE is followed by :stdout (e.g. "html:stdout", "json:stdout"), then the report is written to standard out.
* If the report filename is omitted, the default filename is used ("CodeNarcReport.html").
* If no report option is specified, defaults to a single 'html' report with the default filename.
*
* - plugins - The optional list of CodeNarcPlugin class names to register, separated by commas.
*
*
* @author Chris Mair
* @author Nicolas Vuillamy
*/
@SuppressWarnings(['Println', 'PrintStackTrace'])
class CodeNarc {
protected static final String HELP = """CodeNarc - static analysis for Groovy,
Usage: java org.codenarc.CodeNarc [OPTIONS]
where OPTIONS are zero or more command-line options of the form "-NAME[=VALUE]":
-basedir=
The base (root) directory for the source code to be analyzed.
Defaults to the current directory (".").
-includes=
The comma-separated list of Ant-style file patterns specifying files that must
be included. Defaults to "**/*.groovy".
-excludes=
The comma-separated list of Ant-style file patterns specifying files that must
be excluded. No files are excluded when omitted.
-sourcefiles=
The comma-separated list of files we want to analyze. If set, -includes and -exclude arguments are ignored.
-rulesetfiles=
The path to the Groovy or XML RuleSet definition files, relative to the classpath.
This can be a single file path, or multiple paths separated by commas. Each path may be optionally prefixed by
any of the valid java.net.URL prefixes, such as "file:" (to load from a relative or absolute filesystem path),
or "http:". If it is a URL, its path may be optionally URL-encoded. That can be useful if the path contains
any problematic characters, such as comma (',') or hash ('#'). For instance:
file:src/test/resources/RuleSet-,#.txt
can be encoded as:
file:src%2Ftest%2Fresources%2FRuleSet-%2C%23.txt
See URLEncoder#encode(java.lang.String, java.lang.String). Defaults to "rulesets/basic.xml"
-ruleset=JSON_STRING
String containing a ruleSet in JSON format (if set, rulesetfiles argument will be ignored)
The JSON string must be URL-encoded in UTF-8 before being sent as argument to CodeNarc
-excludeBaseline=
The filename of the optional baseline. If not set, no baseline will be used.
-maxPriority1Violations=
The maximum number of priority 1 violations allowed (int).
-maxPriority2Violations=
The maximum number of priority 2 violations allowed (int).
-maxPriority3Violations=
The maximum number of priority 3 violations allowed (int).
-failOnError=true/false
Whether to terminate and fail the task if any errors occur parsing source files (true), or just log the errors (false). It defaults to false.
-title=
The title for this analysis; used in the output report(s), if supported by the report type. Optional.
-report=
The definition of the report to produce. The option value is of the form
TYPE[:FILENAME], where TYPE is "html", "text", "xml", or "console" and FILENAME is the filename (with
optional path) of the output report filename. If the TYPE is followed by :stdout (e.g. "html:stdout", "json:stdout"),
then the report is written to standard out. If the report filename is omitted, the default filename
is used for the specified report type ("CodeNarcReport.html" for "html", "CodeNarcXmlReport.xml" for
"xml" and "CodeNarcJsonReport.json" for "json"). If no report option is specified, default to a
single "html" report with the default filename.
-plugins=
The optional list of CodeNarcPlugin class names to register, separated by commas.
-help
Display the command-line help. If present, this must be the only command-line parameter.
Example command-line invocations:
java org.codenarc.CodeNarc
java org.codenarc.CodeNarc -rulesetfiles="rulesets/basic.xml" title="My Project"
java org.codenarc.CodeNarc -rulesetfiles="rulesets/basic.xml" -report=baseline:codenarc-baseline.xml
java org.codenarc.CodeNarc -rulesetfiles="rulesets/basic.xml" -excludeBaseline=file:codenarc-baseline.xml
java org.codenarc.CodeNarc -report=xml:MyXmlReport.xml -report=html
java org.codenarc.CodeNarc -report=json:stdout
java org.codenarc.CodeNarc -help'"""
// Abstract calling System.exit() to allow substitution of test spy for unit tests
protected static Closure systemExit = { exitCode -> System.exit(exitCode) }
protected String ruleSetFiles
protected String ruleset
protected String baseDir
protected String includes
protected String excludes
protected String sourceFiles
protected String title
protected String plugins
protected String propertiesFilename
protected List reports = []
/**
* The path to a Baseline Violations report (report type "baseline"). If set, then all violations specified
* within that report are excluded (filtered) from the current CodeNarc run. If null/empty, then do nothing.
*/
String excludeBaseline
/**
* Whether to terminate and fail the task if errors occur parsing source files (true), or just log the errors (false)
*/
boolean failOnError = false
private final ResourceFactory resourceFactory = new DefaultResourceFactory()
// Abstract creation of the CodeNarcRunner instance to allow substitution of test spy for unit tests
protected Closure createCodeNarcRunner = {
def codeNarcRunner = new CodeNarcRunner()
if (excludeBaseline) {
println "Loading baseline violations from [$excludeBaseline]"
def resource = resourceFactory.getResource(excludeBaseline)
def baselinePlugin = new BaselineResultsPlugin(resource)
codeNarcRunner.registerPlugin(baselinePlugin)
}
return codeNarcRunner
}
protected int maxPriority1Violations = Integer.MAX_VALUE
protected int maxPriority2Violations = Integer.MAX_VALUE
protected int maxPriority3Violations = Integer.MAX_VALUE
/**
* Main command-line entry-point. Run the CodeNarc application.
* @param args - the String[] of command-line arguments
*/
static void main(String[] args) {
def codeNarc = new CodeNarc()
// Show help
if (args == ['-help'] as String[]) {
println HELP
return
}
// Show version
else if (args == ['-version'] as String[]) {
def version = CodeNarcVersion.getVersion()
println "CodeNarc version $version"
return
}
try {
codeNarc.execute(args)
}
catch (Throwable t) {
println "ERROR: $t"
t.printStackTrace()
println HELP
systemExit(1)
}
}
protected void execute(String[] args) {
parseArgs(args)
setDefaultsIfNecessary()
def sourceAnalyzer = createSourceAnalyzer()
reports.each { reportWriter ->
if (reportWriter.hasProperty('title')) {
reportWriter.title = title
}
}
def codeNarcRunner = createCodeNarcRunner()
codeNarcRunner.ruleSetFiles = ruleSetFiles
codeNarcRunner.ruleSetString = ruleset
codeNarcRunner.reportWriters = reports
codeNarcRunner.sourceAnalyzer = sourceAnalyzer
codeNarcRunner.propertiesFilename = propertiesFilename
if (plugins) {
codeNarcRunner.registerPluginsForClassNames(plugins)
}
Results results = codeNarcRunner.execute()
checkMaxViolations(results, 1, maxPriority1Violations)
checkMaxViolations(results, 2, maxPriority2Violations)
checkMaxViolations(results, 3, maxPriority3Violations)
}
protected void setDefaultsIfNecessary() {
if (!sourceFiles) {
includes = includes ?: '**/*.groovy'
}
baseDir = baseDir ?: '.'
ruleSetFiles = ruleSetFiles ?: 'rulesets/basic.xml'
if (reports.empty) {
reports << new HtmlReportWriter(title:title)
}
}
protected void checkMaxViolations(Results results, int priority, int max) {
def numViolations = results.getNumberOfViolationsWithPriority(priority, true)
if (numViolations > max) {
println "ERROR: Number of p${priority} violations greater than maximum of $max"
systemExit(1)
}
}
/**
* Create and return the SourceAnalyzer
* @return a configured SourceAnalyzer instance
*/
protected SourceAnalyzer createSourceAnalyzer() {
// List of files sent as argument
if (sourceFiles) {
return new FilesSourceAnalyzer(
baseDirectory: baseDir,
sourceFiles: sourceFiles.split(','),
failOnError: failOnError
)
}
// Files will be listed using base directory, includes & excludes
return new FilesystemSourceAnalyzer(
baseDirectory: baseDir,
includes: includes,
excludes: excludes,
failOnError: failOnError
)
}
protected void parseArgs(String[] args) {
args.each { arg ->
final pattern = /\-(.*)\=(.*)/ // -name=value
def matcher = arg =~ pattern
assert matcher, "Invalid argument format: [$arg]"
def name = matcher[0][1]
def value = matcher[0][2]
switch (name) {
case 'rulesetfiles': ruleSetFiles = value; break
case 'ruleset': ruleset = URLDecoder.decode(value, 'UTF-8') ; break
case 'excludeBaseline': excludeBaseline = value; break
case 'basedir': baseDir = value; break
case 'includes': includes = value; break
case 'excludes': excludes = value; break
case 'sourcefiles': sourceFiles = value ; break
case 'title': title = value; break
case 'report': parseReport(value); break
case 'maxPriority1Violations': maxPriority1Violations = value as int; break
case 'maxPriority2Violations': maxPriority2Violations = value as int; break
case 'maxPriority3Violations': maxPriority3Violations = value as int; break
case 'plugins': plugins = value; break
case 'properties': propertiesFilename = value; break
case 'failOnError': failOnError = Boolean.parseBoolean(value); break
default: throw new IllegalArgumentException("Invalid option: [$arg]")
}
}
}
private void parseReport(String argValue) {
def parts = argValue.split(':', 2)
def type = parts[0]
def reportWriter = new ReportWriterFactory().getReportWriter(type)
if (parts.size() > 1 && parts[1]) {
// Output in stdout (default)
if (parts[1] == 'stdout') {
reportWriter.writeToStandardOut = true
// JSON called via command line must be returned as single line for easier parsing
if (reportWriter instanceof JsonReportWriter) {
reportWriter.writeAsSingleLine = true
}
}
else {
// Output file
reportWriter.outputFile = parts[1]
}
}
reports << reportWriter
}
}