package org.infernus.idea.checkstyle.csapi;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiInvalidElementAccessException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.infernus.idea.checkstyle.checker.Problem;
import org.infernus.idea.checkstyle.checks.Check;
import org.jetbrains.annotations.NotNull;
public class ProcessResultsThread implements Runnable {
private static final Log LOG = LogFactory.getLog(ProcessResultsThread.class);
private final boolean suppressErrors;
private final List<Check> checks;
private final int tabWidth;
private final Optional<String> baseDir;
private final List<Issue> errors;
private final Map<String, PsiFile> fileNamesToPsiFiles;
private final Map<PsiFile, List<Problem>> problems = new HashMap<>();
private static final class Position {
private final boolean afterEndOfLine;
private final int offset;
public static Position at(final int offset, final boolean afterEndOfLine) {
return new Position(offset, afterEndOfLine);
}
public static Position at(final int offset) {
return new Position(offset, false);
}
private Position(final int offset, final boolean afterEndOfLine) {
this.offset = offset;
this.afterEndOfLine = afterEndOfLine;
}
private PsiElement element(final PsiFile psiFile) {
return psiFile.findElementAt(offset);
}
}
public ProcessResultsThread(final boolean suppressErrors,
final List<Check> checks,
final int tabWidth,
final Optional<String> baseDir,
final List<Issue> errors,
final Map<String, PsiFile> fileNamesToPsiFiles) {
this.suppressErrors = suppressErrors;
this.checks = checks;
this.tabWidth = tabWidth;
this.baseDir = baseDir;
this.errors = errors;
this.fileNamesToPsiFiles = fileNamesToPsiFiles;
}
@Override
public void run() {
final Map<PsiFile, List<Integer>> lineLengthCachesByFile = new HashMap<>();
for (final Issue event : errors) {
final PsiFile psiFile = fileNamesToPsiFiles.get(filenameFrom(event));
if (psiFile == null) {
if (LOG.isInfoEnabled()) {
LOG.info("Could not find mapping for file: " + event.fileName + " in " + fileNamesToPsiFiles);
}
return;
}
List<Integer> lineLengthCache = lineLengthCachesByFile.get(psiFile);
if (lineLengthCache == null) {
// we cache the offset of each line as it is created, so as to
// avoid retreating ground we've already covered.
lineLengthCache = new ArrayList<>();
lineLengthCache.add(0); // line 1 is offset 0
lineLengthCachesByFile.put(psiFile, lineLengthCache);
}
processEvent(psiFile, lineLengthCache, event);
}
}
private String filenameFrom(final Issue event) {
final String fileName = baseDir
.map(prefix -> withTrailingSeparator(prefix) + event.fileName)
.orElseGet(() -> event.fileName);
return Paths.get(fileName).normalize().toString();
}
private String withTrailingSeparator(final String path) {
if (path != null && !path.endsWith(File.separator)) {
return path + File.separator;
}
return path;
}
private void processEvent(final PsiFile psiFile, final List<Integer> lineLengthCache, final Issue event) {
if (additionalChecksFail(psiFile, event)) {
return;
}
final Position position = findPosition(lineLengthCache, event, psiFile.textToCharArray());
final PsiElement victim = position.element(psiFile);
if (victim != null) {
addProblemTo(victim, psiFile, event, position.afterEndOfLine);
} else {
addProblemTo(psiFile, psiFile, event, false);
LOG.debug("Couldn't find victim for error: " + event.fileName + "(" + event.lineNumber + ":"
+ event.columnNumber + ") " + event.message);
}
}
private void addProblemTo(final PsiElement victim,
final PsiFile psiFile,
@NotNull final Issue event,
final boolean afterEndOfLine) {
try {
addProblem(psiFile, new Problem(victim, event.message, event.severityLevel, event.lineNumber,
event.columnNumber, afterEndOfLine, suppressErrors));
} catch (PsiInvalidElementAccessException e) {
LOG.error("Element access failed", e);
}
}
private boolean additionalChecksFail(final PsiFile psiFile, final Issue event) {
for (final Check check : checks) {
if (!check.process(psiFile, event.sourceName)) {
return true;
}
}
return false;
}
@NotNull
private Position findPosition(final List<Integer> lineLengthCache, final Issue event, final char[] text) {
if (event.lineNumber == 0) {
return Position.at(event.columnNumber);
} else if (event.lineNumber <= lineLengthCache.size()) {
return Position.at(lineLengthCache.get(event.lineNumber - 1) + event.columnNumber);
} else {
return searchFromEndOfCachedData(lineLengthCache, event, text);
}
}
@NotNull
private Position searchFromEndOfCachedData(final List<Integer> lineLengthCache,
final Issue event,
final char[] text) {
final Position position;
int offset = lineLengthCache.get(lineLengthCache.size() - 1);
boolean afterEndOfLine = false;
int line = lineLengthCache.size();
int column = 0;
for (int i = offset; i < text.length; ++i) {
final char character = text[i];
// for linefeeds we need to handle CR, LF and CRLF,
// hence we accept either and only trigger a new
// line on the LF of CRLF.
final char nextChar = nextCharacter(text, i);
if (character == '\n' || character == '\r' && nextChar != '\n') {
++line;
++offset;
lineLengthCache.add(offset);
column = 0;
} else if (character == '\t') {
column += tabWidth;
++offset;
} else {
++column;
++offset;
}
if (event.lineNumber == line && event.columnNumber == column) {
if (column == 0 && Character.isWhitespace(nextChar)) {
afterEndOfLine = true;
}
break;
}
}
position = Position.at(offset, afterEndOfLine);
return position;
}
private char nextCharacter(final char[] text, final int i) {
if ((i + 1) < text.length) {
return text[i + 1];
}
return '\0';
}
@NotNull
public Map<PsiFile, List<Problem>> getProblems() {
return Collections.unmodifiableMap(problems);
}
private void addProblem(final PsiFile psiFile, final Problem problem) {
List<Problem> problemsForFile = problems.get(psiFile);
if (problemsForFile == null) {
problemsForFile = new ArrayList<>();
problems.put(psiFile, problemsForFile);
}
problemsForFile.add(problem);
}
}