package com.googlecode.jslint4java.cli;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;
import com.googlecode.jslint4java.Issue;
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;
/**
* A command line interface to {@link JSLint}.
*
* @author dom
*/
class Main {
private static final class ParameterDescriptionComparator implements
Comparator<ParameterDescription> {
public int compare(ParameterDescription a, ParameterDescription b) {
return a.getLongestName().compareTo(b.getLongestName());
}
}
/**
* The default command line output.
*/
private static final class DefaultFormatter implements JSLintResultFormatter {
public String format(JSLintResult result) {
if (result.getIssues().isEmpty()) {
return "";
}
String nl = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder();
for (Issue issue : result.getIssues()) {
sb.append(PROGNAME);
sb.append(':');
sb.append(issue.toString());
sb.append(nl);
}
// Strip trailing newline if present. The interface is wrong; we should return
// a list of lines, not a String.
sb.delete(sb.length() - nl.length(), sb.length());
return sb.toString();
}
public String footer() {
return null;
}
public String header() {
return null;
}
}
/**
* This is just to avoid calling {@link System#exit(int)} outside of main()…
*/
// @VisibleForTesting
@SuppressWarnings("serial")
public static class DieException extends RuntimeException {
private final int code;
public DieException(String message, int code) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
private static final String FULL_PROGRAM_NAME = "jslint4java";
private static final String PROGNAME = "jslint";
/**
* The main entry point. Try passing in "--help" for more details.
*
* @param args
* One or more JavaScript files.
* @throws IOException
*/
public static void main(String[] args) throws IOException {
try {
System.exit(new Main().run(args));
} catch (DieException e) {
if (e.getMessage() != null) {
System.err.println(PROGNAME + ": " + e.getMessage());
}
System.exit(e.getCode());
}
}
// @VisibleForTesting.
int run(String[] args) throws IOException {
List<String> files = processOptions(args);
if (formatter.header() != null) {
info(formatter.header());
}
for (String file : files) {
lintFile(file);
}
if (formatter.footer() != null) {
info(formatter.footer());
}
return isErrored() ? 1 : 0;
}
private Charset encoding = Charset.defaultCharset();
private boolean errored = false;
private JSLintResultFormatter formatter;
private JSLint lint;
private final JSLintBuilder lintBuilder = new JSLintBuilder();
private void die(String message) {
throw new DieException(message, 1);
}
/**
* Fetch the named {@link Option}, or null if there is no matching one.
*/
private Option getOption(String optName) {
try {
return Option.valueOf(optName);
} catch (IllegalArgumentException e) {
return null;
}
}
private void info(String message) {
System.out.println(message);
}
private boolean isErrored() {
return errored;
}
// Eclipse's static analysis thinks I never close the UnicodeBomInputStream below.
private void lintFile(String file) throws IOException {
BufferedReader reader = null;
try {
reader = readerForFile(file);
JSLintResult result = lint.lint(file, reader);
String msg = formatter.format(result);
if (msg.length() > 0) {
info(msg);
}
if (!result.getIssues().isEmpty()) {
setErrored(true);
}
} catch (FileNotFoundException e) {
die(file + ": No such file or directory.");
} finally {
if (reader != null) {
reader.close();
}
}
}
/**
* Return a {@link BufferedReader} for {@code file}. If {@code file} is "-" then stdin will be
* used instead.
*/
@SuppressWarnings("resource")
private BufferedReader readerForFile(String file) throws IOException, FileNotFoundException {
InputStream inputStream = "-".equals(file) ? System.in : new FileInputStream(file);
return new BufferedReader(new InputStreamReader(
new UnicodeBomInputStream(inputStream).skipBOM(), encoding));
}
private JSLint makeLint(Flags flags) {
try {
if (flags.timeout > 0) {
lintBuilder.timeout(flags.timeout);
}
if (flags.jslint != null) {
return lintBuilder.fromFile(new File(flags.jslint));
} else {
return lintBuilder.fromDefault();
}
} catch (IOException e) {
die(e.getMessage());
}
return null;
}
private List<String> processOptions(String[] args) {
JSLintFlags jslintFlags = new JSLintFlags();
Flags flags = new Flags();
JCommander jc = new JCommander(new Object[] { flags , jslintFlags });
jc.setProgramName(FULL_PROGRAM_NAME);
try {
jc.parse(args);
} catch (ParameterException e) {
info(e.getMessage());
usage(jc);
}
if (flags.version) {
version();
}
if (flags.help) {
usage(jc);
}
if (flags.encoding != null) {
encoding = flags.encoding;
}
lint = makeLint(flags);
setResultFormatter(flags.report);
for (ParameterDescription pd : jc.getParameters()) {
Parameterized p = pd.getParameterized();
// Is it declared on JSLintFlags?
if (!pd.getObject().getClass().isAssignableFrom(JSLintFlags.class)) {
continue;
}
try {
// Need to get Option.
Option o = getOption(p.getName());
// Need to get value.
Object val = p.get(jslintFlags);
if (val == null) {
continue;
}
Class<?> type = p.getType();
if (type.isAssignableFrom(Boolean.class)) {
lint.addOption(o);
}
// In theory, everything else should be a String for later parsing.
else if (type.isAssignableFrom(String.class)) {
lint.addOption(o, (String) val);
} else {
die("unknown type \"" + type + "\" (for " + p.getName() + ")");
}
} catch (IllegalArgumentException e) {
die(e.getMessage());
}
}
if (flags.files.isEmpty()) {
usage(jc);
return null; // can never happen
} else {
return flags.files;
}
}
private void setErrored(boolean errored) {
this.errored = errored;
}
private void setResultFormatter(String reportType) {
if (reportType == null || reportType.equals("")) {
// The original CLI behaviour: one-per-line, with prefix.
formatter = new DefaultFormatter();
} else if (reportType.equals("plain")) {
formatter = new PlainFormatter();
} else if (reportType.equals("xml")) {
formatter = new JSLintXmlFormatter();
} else if (reportType.equals("junit")) {
formatter = new JUnitXmlFormatter();
} else if (reportType.equals("report")) {
formatter = new ReportFormatter();
} else if (reportType.equals("checkstyle")) {
formatter = new CheckstyleXmlFormatter();
} else {
die("unknown report type '" + reportType + "'");
}
}
private void usage(JCommander jc) {
info(String.format("Usage: %s [options] file.js ...", FULL_PROGRAM_NAME));
info("");
List<ParameterDescription> parameters = jc.getParameters();
String spec = " %-" + getLongestName(parameters) + "s %s";
Collections.sort(parameters, new ParameterDescriptionComparator());
for (ParameterDescription pd : parameters) {
info(String.format(spec, pd.getLongestName(), pd.getDescription()));
}
info("");
version();
}
/** Return the length of the longest parameter name. */
private int getLongestName(List<ParameterDescription> parameters) {
int length = 0;
for (ParameterDescription pd : parameters) {
if (pd.getLongestName().length() > length) {
length = pd.getLongestName().length();
}
}
return length;
}
private void version() {
// TODO: display jslint4java version as well.
if (lint == null) {
lint = lintBuilder.fromDefault();
}
info("using jslint version " + lint.getEdition());
throw new DieException(null, 0);
}
}