package com.haskforce.highlighting.annotation.external;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.haskforce.cabal.completion.CabalFileFinder;
import com.haskforce.cabal.lang.psi.CabalFile;
import com.haskforce.cabal.query.BuildInfo;
import com.haskforce.cabal.query.BuildInfoUtil;
import com.haskforce.cabal.query.CabalQuery;
import com.haskforce.features.intentions.IgnoreHLint;
import com.haskforce.highlighting.annotation.HaskellAnnotationHolder;
import com.haskforce.highlighting.annotation.HaskellProblem;
import com.haskforce.highlighting.annotation.Problems;
import com.haskforce.psi.HaskellFile;
import com.haskforce.settings.ToolKey;
import com.haskforce.ui.tools.HaskellToolsConsole;
import com.haskforce.utils.*;
import com.haskforce.utils.ExecUtil.ExecError;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.configurations.ParametersList;
import com.intellij.lang.annotation.Annotation;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import scala.Option;
import scala.runtime.AbstractFunction1;
import scala.util.Either;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Encapsulation of HLint internals. Uses hlint --json for precise error regions.
* Still functional with older hlints, but error regions are less precise.
*/
public class HLint {
private static final Logger LOG = Logger.getInstance(HLint.class);
private static final VersionTriple HLINT_MIN_VERSION_WITH_JSON_SUPPORT = new VersionTriple(1, 9, 1);
private static final Gson gson = new GsonBuilder().create();
@NotNull
public static Problems lint(final @NotNull Project project, @NotNull String workingDirectory,
@NotNull String file, @NotNull HaskellFile haskellFile) {
final HaskellToolsConsole toolConsole = HaskellToolsConsole.get(project);
final String hlintPath = ToolKey.HLINT_KEY.getPath(project);
final String hlintFlags = ToolKey.HLINT_KEY.getFlags(project);
if (hlintPath == null) return new Problems();
return parseProblems(toolConsole, workingDirectory, hlintPath, hlintFlags, file, haskellFile).fold(
new AbstractFunction1<ExecError, Problems>() {
@Override
public Problems apply(ExecError e) {
toolConsole.writeError(ToolKey.HLINT_KEY, e.getMessage());
NotificationUtil.displayToolsNotification(
NotificationType.ERROR, project, "hlint", e.getMessage()
);
return new Problems();
}
},
FunctionUtil.<Problems>identity()
);
}
@NotNull
public static Either<ExecError, Problems> parseProblems(final HaskellToolsConsole toolConsole,
final @NotNull String workingDirectory,
final @NotNull String path,
final @NotNull String flags,
final @NotNull String file,
final @NotNull HaskellFile haskellFile) {
return getVersion(toolConsole, workingDirectory, path).fold(
new AbstractFunction1<ExecError, Either<ExecError, Problems>>() {
@Override
public Either<ExecError, Problems> apply(ExecError e) {
return e.toLeft();
}
},
new AbstractFunction1<VersionTriple, Either<ExecError, Problems>>() {
@Override
public Either<ExecError, Problems> apply(VersionTriple version) {
final boolean useJson = version.gte(HLINT_MIN_VERSION_WITH_JSON_SUPPORT);
final String[] params = getParams(file, haskellFile, useJson);
return EitherUtil.rightMap(runHlint(
toolConsole, workingDirectory, path, flags, params
), new AbstractFunction1<String, Problems>() {
@Override
public Problems apply(String stdout) {
toolConsole.writeOutput(ToolKey.HLINT_KEY, stdout);
if (useJson) return parseProblemsJson(stdout);
return parseProblemsFallback(stdout);
}
});
}
}
);
}
private static String[] getParams(@NotNull String file,
@NotNull HaskellFile haskellFile,
boolean useJson) {
final List<String> result = new ArrayList<>(1);
result.add(file);
if (useJson) result.add("--json");
result.addAll(getParamsFromCabal(haskellFile));
return result.toArray(new String[0]);
}
private static List<String> getParamsFromCabal(@NotNull HaskellFile haskellFile) {
return BuildInfoUtil.getExtensionOpts(BuildInfoUtil.getBuildInfo(haskellFile));
}
/**
* Parse problems from the hlint --json output.
*/
@NotNull
public static Problems parseProblemsJson(@NotNull String stdout) {
final Problem[] problems = gson.fromJson(stdout, Problem[].class);
if (problems == null) {
LOG.warn("Unable to parse hlint json output: " + stdout);
return new Problems();
}
return new Problems(problems);
}
/**
* Parse a single problem from the old hlint output if json is not supported.
*/
@Nullable
public static Problem parseProblemFallback(String lint) {
List<String> split = StringUtil.split(lint, ":");
if (split.size() < 5) {
return null;
}
int line = StringUtil.parseInt(split.get(1), 0);
if (line == 0) {
return null;
}
int column = StringUtil.parseInt(split.get(2), 0);
if (column == 0) {
return null;
}
String hint = StringUtil.split(split.get(4), "\n").get(0);
split = StringUtil.split(lint, "\n");
split = ContainerUtil.subList(split, 2);
split = StringUtil.split(StringUtil.join(split, "\n"), "Why not:");
if (split.size() != 2) {
return null;
}
final String from = split.get(0).trim();
final String to = split.get(1).trim();
return new Problem("", "", hint, from, to, "", new String[]{}, "", line, column);
}
/**
* Parse problems from the old hlint output if json is not supported.
*/
@NotNull
public static Problems parseProblemsFallback(String stdout) {
final List<String> lints = StringUtil.split(stdout, "\n\n");
Problems problems = new Problems();
for (String lint : lints) {
ContainerUtil.addIfNotNull(problems, parseProblemFallback(lint));
}
return problems;
}
private static final Pattern HLINT_VERSION_REGEX = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)");
@NotNull
private static Either<ExecError, VersionTriple> getVersion(HaskellToolsConsole toolConsole, String workingDirectory, String hlintPath) {
return EitherUtil.rightFlatMap(
runHlint(toolConsole, workingDirectory, hlintPath, "--version"),
new AbstractFunction1<String, Either<ExecError, VersionTriple>>() {
@Override
public Either<ExecError, VersionTriple> apply(String version) {
Matcher m = HLINT_VERSION_REGEX.matcher(version);
if (!m.find()) {
return new ExecError(
"Could not parse version from hlint: '" + version + "'",
null
).toLeft();
}
return EitherUtil.right(new VersionTriple(
Integer.parseInt(m.group(1)),
Integer.parseInt(m.group(2)),
Integer.parseInt(m.group(3))
));
}
}
);
}
/**
* Runs hlintProg with parameters if hlintProg can be executed.
*/
@NotNull
private static Either<ExecError, String> runHlint(HaskellToolsConsole toolConsole,
@NotNull String workingDirectory,
@NotNull String hlintProg,
@NotNull String hlintFlags,
@NotNull String... params) {
GeneralCommandLine commandLine = new GeneralCommandLine();
commandLine.setWorkDirectory(workingDirectory);
commandLine.setExePath(hlintProg);
ParametersList parametersList = commandLine.getParametersList();
// Required so that hlint won't report a non-zero exit status for lint issues.
// Otherwise, ExecUtil.readCommandLine will return an error.
parametersList.add("--no-exit-code");
parametersList.addParametersString(hlintFlags);
parametersList.addAll(params);
toolConsole.writeInput(ToolKey.HLINT_KEY, "Using working directory: " + workingDirectory);
toolConsole.writeInput(ToolKey.HLINT_KEY, commandLine.getCommandLineString());
return ExecUtil.readCommandLine(commandLine);
}
public static class Problem extends HaskellProblem {
public String decl;
public String file;
public String hint;
public String from;
public String to;
public String module;
public String[] note;
public String severity;
public int endLine;
public int endColumn;
public boolean useJson;
/**
* Provide a default constructor so gson objects will default to `useJson = true`.
*/
public Problem() {
useJson = true;
}
public Problem(String decl, String file, String hint, String from, String to, String module, String[] note,
String severity, int startLine, int startColumn, int endLine, int endColumn) {
this.decl = decl;
this.file = file;
this.from = from;
this.hint = hint;
this.module = module;
this.note = note;
this.severity = severity;
this.startLine = startLine;
this.startColumn = startColumn;
this.endLine = endLine;
this.endColumn = endColumn;
this.to = to;
}
public Problem(String decl, String file, String hint, String from, String to, String module, String[] note,
String severity, int startLine, int startColumn) {
this(decl, file, hint, from, to, module, note, severity, startLine, startColumn, -1, -1);
useJson = false;
}
public String getMessage() {
return hint + (to == null || to.isEmpty() ? "" : ", why not: " + to);
}
protected void createAnnotation(@NotNull HaskellAnnotationHolder holder, int start, int end, @NotNull String message) {
Annotation ann = holder.createWarningAnnotation(TextRange.create(start, end), message);
if (ann != null) ann.registerFix(new IgnoreHLint(hint));
}
@Override
public void createAnnotations(@NotNull PsiFile file, @NotNull HaskellAnnotationHolder holder) {
final String text = file.getText();
final int start = getOffsetStart(text);
final int end = getOffsetEnd(start, text);
if (start == -1 || end == -1) {
return;
}
if (useJson && hint.equals("Use camelCase")) {
createUseCamelCaseAnnotations(text, start, end, holder);
} else {
createDefaultAnnotations(start, end, holder);
}
}
public static final Pattern NON_CAMEL_CASE_REGEX = Pattern.compile("\\b\\w+_\\w+\\b");
public void createUseCamelCaseAnnotations(@NotNull String text, int start, int end, @NotNull HaskellAnnotationHolder holder) {
final String section = text.substring(start, end);
Matcher m = NON_CAMEL_CASE_REGEX.matcher(section);
if (m.find()) {
do {
createAnnotation(holder, start + m.start(), start + m.end(), "Use camelCase");
} while (m.find());
} else {
createDefaultAnnotations(start, end, holder);
}
}
public void createDefaultAnnotations(int start, int end, @NotNull HaskellAnnotationHolder holder) {
createAnnotation(holder, start, end, getMessage());
}
public int getOffsetEnd(int offsetStart, String fileText) {
final int offsetEnd = useJson ? StringUtil.lineColToOffset(fileText, endLine - 1, endColumn - 1)
: getOffsetEndFallback(offsetStart, fileText);
return offsetEnd == -1 ? fileText.length() : offsetEnd;
}
private static final Pattern WHITESPACE_REGEX = Pattern.compile("\\s+");
/**
* Fallback to a crude guess if the json output is not available from hlint.
*/
public int getOffsetEndFallback(int offsetStart, String fileText) {
int width = 0;
int nonWhiteSpaceToFind = WHITESPACE_REGEX.matcher(from).replaceAll("").length();
int nonWhiteSpaceFound = 0;
while (offsetStart + width < fileText.length()) {
final char c = fileText.charAt(offsetStart + width);
if (StringUtil.isLineBreak(c)) {
break;
}
if (!StringUtil.isWhiteSpace(c)) {
++nonWhiteSpaceFound;
}
++width;
if (nonWhiteSpaceFound >= nonWhiteSpaceToFind) {
break;
}
}
return offsetStart + width;
}
}
// TODO: VersionTriple may be useful in a util module or there may be an better existing implementation.
public static class VersionTriple {
private final int x;
private final int y;
private final int z;
VersionTriple(final int x_, final int y_, final int z_) {
x = x_;
y = y_;
z = z_;
}
public boolean eq(VersionTriple v) {
return v != null && x == v.x && y == v.y && z == v.z;
}
public boolean gt(VersionTriple v) {
return v != null && (x > v.x || x == v.x && (y > v.y || y == v.y && z > v.z));
}
public boolean lt(VersionTriple v) {
return v != null && (x < v.x || x == v.x && (y < v.y || y == v.y && z < v.z));
}
public boolean gte(VersionTriple v) {
return eq(v) || gt(v);
}
public boolean lte(VersionTriple v) {
return eq(v) || lt(v);
}
}
}