<commit_before>package com.haskforce.highlighting.annotation.external;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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.utils.ExecUtil;
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 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(@NotNull Project project, @NotNull String workingDirectory, @NotNull String file) {
        final String hlintPath = getPath(project);
        if (hlintPath == null) return new Problems();

        return parseProblems(workingDirectory, hlintPath, file);
    }

    public static String getPath(@NotNull Project project) {
        return ExecUtil.getExternalToolPath(project, ExecUtil.HLINT_PATH_KEY);
    }

    @NotNull
    public static Problems parseProblems(@NotNull String workingDirectory, @NotNull String hlintPath, @NotNull String file) {
        VersionTriple version = getVersion(workingDirectory, hlintPath);
        if (version == null) {
            return new Problems();
        }
        final boolean useJson = version.gte(HLINT_MIN_VERSION_WITH_JSON_SUPPORT);
        final String stdout = ExecUtil.readCommandLine(workingDirectory, hlintPath,
                                                       useJson ? new String[]{"--json", file} : new String[]{file});
        if (stdout == null) {
            LOG.warn("Unable to get output from hlint");
            return new Problems();
        }
        if (useJson) {
            return parseProblemsJson(stdout);
        }
        return parseProblemsFallback(stdout);
    }

    /**
     * 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+)");

    @Nullable
    private static VersionTriple getVersion(String workingDirectory, String hlintPath) {
        final String version = ExecUtil.readCommandLine(workingDirectory, hlintPath, "--version");
        if (version == null) {
            return null;
        }
        Matcher m = HLINT_VERSION_REGEX.matcher(version);
        if (!m.find()) {
            return null;
        }
        return new VersionTriple(
                Integer.parseInt(m.group(1)),
                Integer.parseInt(m.group(2)),
                Integer.parseInt(m.group(3)));
    }

    public static class Problem extends HaskellProblem {<commit_msg>Ensure the hlint binary exists before executing it.<commit_after>package com.haskforce.highlighting.annotation.external;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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.utils.ExecUtil;
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 java.io.File;
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(@NotNull Project project, @NotNull String workingDirectory, @NotNull String file) {
        final String hlintPath = getPath(project);
        if (hlintPath == null) return new Problems();

        return parseProblems(workingDirectory, hlintPath, file);
    }

    public static String getPath(@NotNull Project project) {
        return ExecUtil.getExternalToolPath(project, ExecUtil.HLINT_PATH_KEY);
    }

    @NotNull
    public static Problems parseProblems(@NotNull String workingDirectory, @NotNull String hlintPath, @NotNull String file) {
        VersionTriple version = getVersion(workingDirectory, hlintPath);
        if (version == null) {
            return new Problems();
        }
        final boolean useJson = version.gte(HLINT_MIN_VERSION_WITH_JSON_SUPPORT);
        final String stdout = runHlint(workingDirectory, hlintPath,
                                       useJson ? new String[]{"--json", file} : new String[]{file});
        if (stdout == null) {
            LOG.warn("Unable to get output from hlint");
            return new Problems();
        }
        if (useJson) {
            return parseProblemsJson(stdout);
        }
        return parseProblemsFallback(stdout);
    }

    /**
     * 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+)");

    @Nullable
    private static VersionTriple getVersion(String workingDirectory, String hlintPath) {
        final String version = runHlint(workingDirectory, hlintPath, "--version");
        if (version == null) {
            return null;
        }
        Matcher m = HLINT_VERSION_REGEX.matcher(version);
        if (!m.find()) {
            return null;
        }
        return new VersionTriple(
                Integer.parseInt(m.group(1)),
                Integer.parseInt(m.group(2)),
                Integer.parseInt(m.group(3)));
    }

    /**
     * Runs hlintProg with parameter if hlintProg can be executed.
     */
    @Nullable
    private static String runHlint(@NotNull String workingDirectory,
                                   @NotNull String hlintProg,
                                   @NotNull String params) {
        if (!(new File(hlintProg).canExecute())) return null;

        return ExecUtil.readCommandLine(workingDirectory, hlintProg, params);
    }


    /**
     * Runs hlintProg with parameters if hlintProg can be executed.
     */
    @Nullable
    private static String runHlint(@NotNull String workingDirectory,
                                   @NotNull String hlintProg,
                                   @NotNull String[] params) {
        if (!(new File(hlintProg).canExecute())) return null;

        return ExecUtil.readCommandLine(workingDirectory, hlintProg, params);
    }

    public static class Problem extends HaskellProblem {
{'fraction_non_alphanumeric': 0.10763919624679118, 'fraction_numerical': 0.0033637248827122243, 'mean_word_length': 2.60727969348659, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 7, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5525934', 'n_tokens_mistral': 3230, 'n_tokens_neox': 3069, 'n_words': 821}