package com.googlecode.jslint4java.maven;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import com.googlecode.jslint4java.JSLint;
import com.googlecode.jslint4java.JSLintBuilder;
import com.googlecode.jslint4java.JSLintResult;
import com.googlecode.jslint4java.Option;
import com.googlecode.jslint4java.UnicodeBomInputStream;
import com.googlecode.jslint4java.formatter.CheckstyleXmlFormatter;
import com.googlecode.jslint4java.formatter.JSLintResultFormatter;
import com.googlecode.jslint4java.formatter.JSLintXmlFormatter;
import com.googlecode.jslint4java.formatter.JUnitXmlFormatter;
import com.googlecode.jslint4java.formatter.PlainFormatter;
import com.googlecode.jslint4java.formatter.ReportFormatter;
/**
* Validates JavaScript using jslint4java.
*
* @author dom
*/
// TODO Support alternate jslint
// TODO Support HTML reports (site plugin mojo?)
@Mojo(name = "lint", defaultPhase = LifecyclePhase.VERIFY)
public class JSLintMojo extends AbstractMojo {
/** Where to write the HTML report. */
private static final String REPORT_HTML = "report.html";
/** Where to write the plain text report. */
private static final String REPORT_TXT = "report.txt";
/** Where to write the checkstyle report. */
private static final String CHECKSTYLE_XML = "checkstyle.xml";
private static final String DEFAULT_INCLUDES = "**/*.js";
private static final String JSLINT_XML = "jslint.xml";
/** Where to write the junit report. */
private static final String JUNIT_XML = "junit.xml";
/**
* Specifies the the source files to be excluded for JSLint (relative to
* {@link #defaultSourceFolder}). Maven applies its own defaults.
*/
@Parameter(property = "excludes")
private final List<String> excludes = Lists.newArrayList();
/**
* Specifies the the source files to be used for JSLint (relative to
* {@link #defaultSourceFolder}). If none are given, defaults to <code>**/*.js</code>.
*/
@Parameter(property = "includes")
private final List<String> includes = Lists.newArrayList();
/**
* Specifies the location of the default source folder to be used for JSLint. Note that this is
* just used for filling in the default, as it resolves the default value correctly. Anything
* you specify will override it.
*/
@Parameter(readonly = true, required = true, defaultValue = "${basedir}/src/main/webapp")
private File defaultSourceFolder;
/**
* Which locations should JSLint look for JavaScript files in? Defaults to
* ${basedir}/src/main/webapp.
*/
@Parameter
private File[] sourceFolders = new File[] {};
/**
* Which JSLint {@link Option}s to set.
*/
@Parameter
private final Map<String, String> options = Maps.newHashMap();
/**
* What encoding should we use to read the JavaScript files? Defaults to UTF-8.
*/
@Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
private String encoding = "UTF-8";
/**
* Base folder for report output.
*/
@Parameter(property = "jslint.outputFolder", defaultValue = "${project.build.directory}/jslint4java")
private File outputFolder = new File("target");
/**
* Fail the build if JSLint detects any problems.
*/
@Parameter(property = "jslint.failOnError", defaultValue = "true")
private boolean failOnError = true;
/**
* An alternative JSLint to use.
*/
@Parameter(property = "jslint.source")
private File jslintSource;
/**
* How many seconds JSLint is allowed to run.
*/
@Parameter(property = "jslint.timeout")
private long timeout;
/**
* Skip linting files if true.
*/
@Parameter(property = "jslint.skip", defaultValue = "false")
private boolean skip = false;
/** Add a single option. For testing only. */
void addOption(Option sloppy, String value) {
options.put(sloppy.name().toLowerCase(Locale.ENGLISH), value);
}
/**
* Set the default includes.
*/
private void applyDefaults() {
if (includes.isEmpty()) {
includes.add(DEFAULT_INCLUDES);
}
if (sourceFolders.length == 0) {
sourceFolders = new File[] { defaultSourceFolder };
}
}
private void applyOptions(JSLint jsLint) throws MojoExecutionException {
for (Entry<String, String> entry : options.entrySet()) {
if (entry.getValue() == null || entry.getValue().equals("")) {
continue;
}
Option option;
try {
option = Option.valueOf(entry.getKey().toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
throw new MojoExecutionException("unknown option: " + entry.getKey());
}
jsLint.addOption(option, entry.getValue());
}
}
public void execute() throws MojoExecutionException, MojoFailureException {
if (skip) {
getLog().info("skipping JSLint");
return;
}
JSLint jsLint = applyJSlintSource();
applyDefaults();
applyOptions(jsLint);
List<File> files = getFilesToProcess();
int failures = 0;
ReportWriter reporter = makeReportWriter();
try {
reporter.open();
for (File file : files) {
JSLintResult result = lintFile(jsLint, file);
failures += result.getIssues().size();
logIssues(result, reporter);
}
} finally {
reporter.close();
}
if (failures > 0) {
String message = "JSLint found " + failures + " problems in " + files.size() + " files";
if (failOnError) {
throw new MojoFailureException(message);
} else {
getLog().info(message);
}
}
}
private JSLint applyJSlintSource() throws MojoExecutionException {
JSLintBuilder builder = new JSLintBuilder();
if (timeout > 0) {
builder.timeout(timeout);
}
if (jslintSource != null) {
try {
return builder.fromFile(jslintSource, Charset.forName(encoding));
} catch (IOException e) {
throw new MojoExecutionException("Cant' load jslint.js", e);
}
} else {
return builder.fromDefault();
}
}
@VisibleForTesting
String getEncoding() {
return encoding;
}
/**
* Process includes and excludes to work out which files we are interested in. Originally nicked
* from CheckstyleReport, now looks nothing like it.
*
* @return a {@link List} of {@link File}s.
*/
private List<File> getFilesToProcess() throws MojoExecutionException {
// Defaults.
getLog().debug("includes=" + includes);
getLog().debug("excludes=" + excludes);
List<File> files = Lists.newArrayList();
for (File folder : sourceFolders) {
getLog().debug("searching " + folder);
try {
files.addAll(new FileLister(folder, includes, excludes).files());
} catch (IOException e) {
// Looking in FileUtils, this is a "can never happen". *sigh*
throw new MojoExecutionException("Error listing files", e);
}
}
return files;
}
@VisibleForTesting
Map<String, String> getOptions() {
return options;
}
private JSLintResult lintFile(JSLint jsLint, File file) throws MojoExecutionException {
getLog().debug("lint " + file);
BufferedReader reader = null;
try {
UnicodeBomInputStream stream = new UnicodeBomInputStream(new FileInputStream(file));
stream.skipBOM();
reader = new BufferedReader(new InputStreamReader(stream, getEncoding()));
return jsLint.lint(file.toString(), reader);
} catch (FileNotFoundException e) {
throw new MojoExecutionException("file not found: " + file, e);
} catch (UnsupportedEncodingException e) {
// Can never happen.
throw new MojoExecutionException("unsupported character encoding UTF-8", e);
} catch (IOException e) {
throw new MojoExecutionException("problem whilst linting " + file, e);
} finally {
Closeables.closeQuietly(reader);
}
}
private void logIssues(JSLintResult result, ReportWriter reporter) {
reporter.report(result);
if (result.getIssues().isEmpty()) {
return;
}
logIssuesToConsole(result);
}
private void logIssuesToConsole(JSLintResult result) {
JSLintResultFormatter formatter = new PlainFormatter();
String report = formatter.format(result);
for (String line : report.split("\n")) {
getLog().info(line);
}
}
private ReportWriter makeReportWriter() {
ReportWriterImpl f1 = new ReportWriterImpl(new File(outputFolder, JSLINT_XML),
new JSLintXmlFormatter());
ReportWriterImpl f2 = new ReportWriterImpl(new File(outputFolder, JUNIT_XML),
new JUnitXmlFormatter());
ReportWriterImpl f3 = new ReportWriterImpl(new File(outputFolder, CHECKSTYLE_XML),
new CheckstyleXmlFormatter());
ReportWriterImpl f4 = new ReportWriterImpl(new File(outputFolder, REPORT_TXT),
new PlainFormatter());
ReportWriterImpl f5 = new ReportWriterImpl(new File(outputFolder, REPORT_HTML),
new ReportFormatter());
return new MultiReportWriter(f1, f2, f3, f4, f5);
}
public void setDefaultSourceFolder(File defaultSourceFolder) {
this.defaultSourceFolder = defaultSourceFolder;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setExcludes(List<String> excludes) {
this.excludes.clear();
this.excludes.addAll(excludes);
}
public void setFailOnError(boolean b) {
failOnError = b;
}
public void setIncludes(List<String> includes) {
this.includes.clear();
this.includes.addAll(includes);
}
/** The location of the JSLint source file. */
public void setJslint(File jslintSource) {
this.jslintSource = jslintSource;
}
public void setOptions(Map<String, String> options) {
this.options.clear();
this.options.putAll(options);
}
public void setOutputFolder(File outputFolder) {
this.outputFolder = outputFolder;
}
public void setSkip(boolean skip) {
this.skip = skip;
}
public void setSourceFolders(List<File> sourceFolders) {
this.sourceFolders = sourceFolders.toArray(new File[sourceFolders.size()]);
}
}