package com.pablissimo.sonar;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.TempFolder;
import org.sonar.api.utils.command.Command;
import org.sonar.api.utils.command.CommandExecutor;
import org.sonar.api.utils.command.StreamConsumer;
import org.sonar.api.utils.command.StringStreamConsumer;
public class TsLintExecutorImpl implements TsLintExecutor {
public static final int MAX_COMMAND_LENGTH = 4096;
private static final Logger LOG = LoggerFactory.getLogger(TsLintExecutorImpl.class);
private boolean mustQuoteSpaceContainingPaths = false;
private TempFolder tempFolder;
public TsLintExecutorImpl(System2 system, TempFolder tempFolder) {
this.mustQuoteSpaceContainingPaths = system.isOsWindows();
this.tempFolder = tempFolder;
}
private String preparePath(String path) {
if (path == null) {
return "";
}
else if (path.contains(" ") && this.mustQuoteSpaceContainingPaths) {
return '"' + path + '"';
}
else {
return path;
}
}
private Command getBaseCommand(TsLintExecutorConfig config, String tempPath) {
Command command =
Command
.create("node")
.addArgument(this.preparePath(config.getPathToTsLint()))
.addArgument("--format")
.addArgument("json");
String rulesDir = config.getRulesDir();
if (rulesDir != null && rulesDir.length() > 0) {
command
.addArgument("--rules-dir")
.addArgument(this.preparePath(rulesDir));
}
if (tempPath != null && tempPath.length() > 0) {
command
.addArgument("--out")
.addArgument(this.preparePath(tempPath));
}
command
.addArgument("--config")
.addArgument(this.preparePath(config.getConfigFile()));
if (config.useTsConfigInsteadOfFileList()) {
command
.addArgument("--project")
.addArgument(this.preparePath(config.getPathToTsConfig()));
}
if (config.shouldPerformTypeCheck()) {
command
.addArgument("--type-check");
}
command.setNewShell(false);
return command;
}
@Override
public List<String> execute(TsLintExecutorConfig config, List<String> files) {
if (config == null) {
throw new IllegalArgumentException("config");
}
else if (files == null) {
throw new IllegalArgumentException("files");
}
if (config.useExistingTsLintOutput()) {
LOG.debug("Running with existing JSON file '{}' instead of calling tslint", config.getPathToTsLintOutput());
List<String> toReturn = new ArrayList<>();
toReturn.add(this.getFileContent(new File(config.getPathToTsLintOutput())));
return toReturn;
}
// New up a command that's everything we need except the files to process
// We'll use this as our reference for chunking up files, if we need to
File tslintOutputFile = this.tempFolder.newFile();
String tslintOutputFilePath = tslintOutputFile.getAbsolutePath();
Command baseCommand = getBaseCommand(config, tslintOutputFilePath);
LOG.debug("Using a temporary path for TsLint output: {}", tslintOutputFilePath);
StringStreamConsumer stdOutConsumer = new StringStreamConsumer();
StringStreamConsumer stdErrConsumer = new StringStreamConsumer();
List<String> toReturn = new ArrayList<>();
if (config.useTsConfigInsteadOfFileList()) {
LOG.debug("Running against a single project JSON file");
// If we're being asked to use a tsconfig.json file, it'll contain
// the file list to lint - so don't batch, and just run with it
toReturn.add(this.getCommandOutput(baseCommand, stdOutConsumer, stdErrConsumer, tslintOutputFile, config.getTimeoutMs()));
}
else {
int baseCommandLength = baseCommand.toCommandLine().length();
int availableForBatching = MAX_COMMAND_LENGTH - baseCommandLength;
List<List<String>> batches = new ArrayList<>();
List<String> currentBatch = new ArrayList<>();
batches.add(currentBatch);
int currentBatchLength = 0;
for (int i = 0; i < files.size(); i++) {
String nextPath = this.preparePath(files.get(i).trim());
// +1 for the space we'll be adding between filenames
if (currentBatchLength + nextPath.length() + 1 > availableForBatching) {
// Too long to add to this batch, create new
currentBatch = new ArrayList<>();
currentBatchLength = 0;
batches.add(currentBatch);
}
currentBatch.add(nextPath);
currentBatchLength += nextPath.length() + 1;
}
LOG.debug("Split {} files into {} batches for processing", files.size(), batches.size());
for (int i = 0; i < batches.size(); i++) {
List<String> thisBatch = batches.get(i);
Command thisCommand = getBaseCommand(config, tslintOutputFilePath);
for (int fileIndex = 0; fileIndex < thisBatch.size(); fileIndex++) {
thisCommand.addArgument(thisBatch.get(fileIndex));
}
LOG.debug("Executing TsLint with command: {}", thisCommand.toCommandLine());
// Timeout is specified per file, not per batch (which can vary a lot)
// so multiply it up
toReturn.add(this.getCommandOutput(thisCommand, stdOutConsumer, stdErrConsumer, tslintOutputFile, config.getTimeoutMs() * thisBatch.size()));
}
}
return toReturn;
}
private String getCommandOutput(Command thisCommand, StreamConsumer stdOutConsumer, StreamConsumer stdErrConsumer, File tslintOutputFile, Integer timeoutMs) {
this.createExecutor().execute(thisCommand, stdOutConsumer, stdErrConsumer, timeoutMs);
return getFileContent(tslintOutputFile);
}
private String getFileContent(File tslintOutputFile) {
StringBuilder outputBuilder = new StringBuilder();
try {
BufferedReader reader = this.getBufferedReaderForFile(tslintOutputFile);
String str;
while ((str = reader.readLine()) != null) {
outputBuilder.append(str);
}
reader.close();
return outputBuilder.toString();
}
catch (IOException ex) {
LOG.error("Failed to re-read TsLint output", ex);
}
return "";
}
protected BufferedReader getBufferedReaderForFile(File file) throws IOException {
return new BufferedReader(
new InputStreamReader(
new FileInputStream(file), "UTF8"));
}
protected CommandExecutor createExecutor() {
return CommandExecutor.create();
}
}