<commit_before>package edu.hm.hafner.analysis;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.Stack;

import edu.hm.hafner.util.LookaheadStream;

/**
 * Parses a report file line by line for issues using a pre-defined regular expression. If the regular expression
 * matches then the abstract method {@link #createIssue(Matcher, LookaheadStream, IssueBuilder)} will be called. Sub
 * classes need to provide an implementation that transforms the {@link Matcher} instance into a new issue. If required,
 * sub classes may consume additional lines from the report file before control is handed back to the template method of
 * this parser.
 *
 * @author <NAME>
 */
public abstract class LookaheadParser extends IssueParser {
    private static final long serialVersionUID = 3240719494150024894L;

    /** Pattern identifying an ant task debug output prefix. */
    protected static final String ANT_TASK = "^(?:.*\\[[^]]*\\])?\\s*";

    private static final String ENTERING_DIRECTORY = "Entering directory";
    private static final String LEAVING_DIRECTORY = "Leaving directory";
    private static final Pattern MAKE_PATH
            = Pattern.compile(".*make(?:\\[\\d+])?: " + ENTERING_DIRECTORY + " [`'](?<dir>.*)['`]");
    private static final String CMAKE_PREFIX = "-- Build files have";
    private static final Pattern CMAKE_PATH = Pattern.compile(CMAKE_PREFIX + " been written to: (?<dir>.*)");

    private static final int MAX_LINE_LENGTH = 4000; // see JENKINS-55805

    private final Pattern pattern;

    private final Stack<String> recursiveMakeDirectories;

    /**
     * Creates a new instance of {@link LookaheadParser}.
     *
     * @param pattern
     *         pattern of compiler warnings.
     */
    protected LookaheadParser(final String pattern) {
        super();

        this.pattern = Pattern.compile(pattern);
        this.recursiveMakeDirectories = new Stack<>();
    }

    @Override
    public Report parse(final ReaderFactory readerFactory) throws ParsingException, ParsingCanceledException {
        Report report = new Report();
        try (Stream<String> lines = readerFactory.readStream()) {
            try (LookaheadStream lookahead = new LookaheadStream(lines, readerFactory.getFileName())) {
                parse(report, lookahead);
            }
        }

        return postProcess(report);
    }

    private void parse(final Report report, final LookaheadStream lookahead) {
        try (IssueBuilder builder = new IssueBuilder()) {
            while (lookahead.hasNext()) {
                String line = lookahead.next();
                handleDirectoryChanges(builder, line);
                if (isLineInteresting(line)) {
                    Matcher matcher = pattern.matcher(line);
                    if (matcher.find()) {
                        createIssue(matcher, lookahead, builder).ifPresent(report::add);
                    }
                }
                if (Thread.interrupted()) {
                    throw new ParsingCanceledException();
                }
            }
        }
    }

    /**
     * Uses Make and CMake output to track directory structure as the compiler moves between source locations.
     *
     * @param builder
     *         {@link IssueBuilder} to set directory for
     * @param line
     *         the line to parse
     */
    private void handleDirectoryChanges(final IssueBuilder builder, final String line) {
        if (line.contains(ENTERING_DIRECTORY)) {
            final String directory = extractDirectory(line, MAKE_PATH);
            recursiveMakeDirectories.push(directory);
            builder.setDirectory(recursiveMakeDirectories.peek());
        }
        else if (line.contains(LEAVING_DIRECTORY) && !recursiveMakeDirectories.isEmpty()) {
            recursiveMakeDirectories.pop();
            if (!recursiveMakeDirectories.isEmpty()) {
                builder.setDirectory(recursiveMakeDirectories.peek());
            }
        }
        else if (line.contains(CMAKE_PREFIX)) {
            builder.setDirectory(extractDirectory(line, CMAKE_PATH));
        }
    }

    /**
     * Extracts a directory from a line using a specified pattern which contains a capture group named 'dir'.
     *
     * @param line
     *         the line to parse using makePath
     * @param makePath
     *         {@link Pattern} which includes a capture group name 'dir'
     *
     * @return A path extracted from the input line
     * @throws IllegalArgumentException
     *         If the {@link Pattern} does not contain a capture group named 'dir'
     * @throws ParsingException
     *         If the {@link Pattern} fails to match the input line
     */
    private String extractDirectory(final String line, final Pattern makePath) throws ParsingException {
        if (!makePath.toString().contains("<dir>")) {
            throw new IllegalArgumentException(makePath.toString() + " does not contain a capture group named 'dir'");
        }
        Matcher makeLineMatcher = makePath.matcher(line);
        if (makeLineMatcher.matches()) {
            return makeLineMatcher.group("dir");
        }
        throw new ParsingException("Unable to change directory using: " + makePath.toString() + " to match " + line);
    }

    /**
     * Creates a new issue for the specified pattern. This method is called for each matching line in the specified
     * file. If a match is a false positive, then return {@link Optional#empty()} to ignore this warning.
     *
     * @param matcher
     *         the regular expression matcher
     * @param lookahead
     *         the lookahead stream to read additional lines
     * @param builder
     *         the issue builder to use
     *
     * @return a new annotation for the specified pattern
     * @throws ParsingException
     *         Signals that during parsing a non recoverable error has been occurred
     */
    protected abstract Optional<Issue> createIssue(Matcher matcher, LookaheadStream lookahead, IssueBuilder builder)
            throws ParsingException;

    /**
     * Returns whether the specified line is interesting. Each interesting line will be matched by the defined regular
     * expression. Here a parser can implement some fast checks (i.e. string or character comparisons) in order to see
     * if a required condition is met. This default implementation does always return {@code true}.
     *
     * @param line
     *         the line to inspect
     *
     * @return {@code true} if the line should be handed over to the regular expression scanner, {@code false} if the
     *         line does not contain a warning.
     */<commit_msg>Addressing PR Comments<commit_after>package edu.hm.hafner.analysis;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.Stack;

import edu.hm.hafner.util.LookaheadStream;

/**
 * Parses a report file line by line for issues using a pre-defined regular expression. If the regular expression
 * matches then the abstract method {@link #createIssue(Matcher, LookaheadStream, IssueBuilder)} will be called. Sub
 * classes need to provide an implementation that transforms the {@link Matcher} instance into a new issue. If required,
 * sub classes may consume additional lines from the report file before control is handed back to the template method of
 * this parser.
 *
 * @author <NAME>
 */
public abstract class LookaheadParser extends IssueParser {
    private static final long serialVersionUID = 3240719494150024894L;

    /** Pattern identifying an ant task debug output prefix. */
    protected static final String ANT_TASK = "^(?:.*\\[[^]]*\\])?\\s*";

    private static final String ENTERING_DIRECTORY = "Entering directory";
    private static final String LEAVING_DIRECTORY = "Leaving directory";
    private static final Pattern MAKE_PATH
            = Pattern.compile(".*make(?:\\[\\d+])?: " + ENTERING_DIRECTORY + " [`'](?<dir>.*)['`]");
    private static final String CMAKE_PREFIX = "-- Build files have";
    private static final Pattern CMAKE_PATH = Pattern.compile(CMAKE_PREFIX + " been written to: (?<dir>.*)");

    private static final int MAX_LINE_LENGTH = 4000; // see JENKINS-55805

    private final Pattern pattern;

    private final Stack<String> recursiveMakeDirectories;

    /**
     * Creates a new instance of {@link LookaheadParser}.
     *
     * @param pattern
     *         pattern of compiler warnings.
     */
    protected LookaheadParser(final String pattern) {
        super();

        this.pattern = Pattern.compile(pattern);
        this.recursiveMakeDirectories = new Stack<>();
    }

    @Override
    public Report parse(final ReaderFactory readerFactory) throws ParsingException, ParsingCanceledException {
        Report report = new Report();
        try (Stream<String> lines = readerFactory.readStream()) {
            try (LookaheadStream lookahead = new LookaheadStream(lines, readerFactory.getFileName())) {
                parse(report, lookahead);
            }
        }

        return postProcess(report);
    }

    private void parse(final Report report, final LookaheadStream lookahead) {
        try (IssueBuilder builder = new IssueBuilder()) {
            while (lookahead.hasNext()) {
                String line = lookahead.next();
                handleDirectoryChanges(builder, line);
                if (isLineInteresting(line)) {
                    Matcher matcher = pattern.matcher(line);
                    if (matcher.find()) {
                        createIssue(matcher, lookahead, builder).ifPresent(report::add);
                    }
                }
                if (Thread.interrupted()) {
                    throw new ParsingCanceledException();
                }
            }
        }
    }

    /**
     * When changing directories using Make output, save new directory to our stack for later use, then return it for
     * use now
     * @param line
     *         the line to parse
     * *
     * @return The new directory to change to
     */
    private String newMakeDirectory(final String line) {
        recursiveMakeDirectories.push(extractDirectory(line, MAKE_PATH));
        return recursiveMakeDirectories.peek();
    }

    /**
     * When changing directories using Make output, set our stack to the last directory seen, and return that directory
     *
     * @return The last directory seen, or an empty String if we have returned to the beginning
     */
    private String lastMakeDirectory() {
        if (!recursiveMakeDirectories.empty()) {
            recursiveMakeDirectories.pop();
            if (!recursiveMakeDirectories.empty()) {
                return recursiveMakeDirectories.peek();
            }
        }
        return "";
    }

    /**
     * Uses Make and CMake output to track directory structure as the compiler moves between source locations.
     *
     * @param builder
     *         {@link IssueBuilder} to set directory for
     * @param line
     *         the line to parse
     */
    private void handleDirectoryChanges(final IssueBuilder builder, final String line) {
        if (line.contains(ENTERING_DIRECTORY)) {
            builder.setDirectory(newMakeDirectory(line));
        }
        else if (line.contains(LEAVING_DIRECTORY) ) {
            builder.setDirectory(lastMakeDirectory());
        }
        else if (line.contains(CMAKE_PREFIX)) {
            builder.setDirectory(extractDirectory(line, CMAKE_PATH));
        }
    }

    /**
     * Extracts a directory from a line using a specified pattern which contains a capture group named 'dir'.
     *
     * @param line
     *         the line to parse using makePath
     * @param makePath
     *         {@link Pattern} which includes a capture group name 'dir'
     *
     * @return A path extracted from the input line
     * @throws IllegalArgumentException
     *         If the {@link Pattern} does not contain a capture group named 'dir'
     * @throws ParsingException
     *         If the {@link Pattern} fails to match the input line
     */
    private String extractDirectory(final String line, final Pattern makePath) throws ParsingException {
        if (!makePath.toString().contains("<dir>")) {
            throw new IllegalArgumentException(
                    String.format("%s does not contain a capture group named 'dir'", makePath.toString()));
        }
        Matcher makeLineMatcher = makePath.matcher(line);
        if (makeLineMatcher.matches()) {
            return makeLineMatcher.group("dir");
        }
        throw new ParsingException(
                String.format("Unable to change directory using: %s to match %s", makePath.toString(), line));
    }

    /**
     * Creates a new issue for the specified pattern. This method is called for each matching line in the specified
     * file. If a match is a false positive, then return {@link Optional#empty()} to ignore this warning.
     *
     * @param matcher
     *         the regular expression matcher
     * @param lookahead
     *         the lookahead stream to read additional lines
     * @param builder
     *         the issue builder to use
     *
     * @return a new annotation for the specified pattern
     * @throws ParsingException
     *         Signals that during parsing a non recoverable error has been occurred
     */
    protected abstract Optional<Issue> createIssue(Matcher matcher, LookaheadStream lookahead, IssueBuilder builder)
            throws ParsingException;

    /**
     * Returns whether the specified line is interesting. Each interesting line will be matched by the defined regular
     * expression. Here a parser can implement some fast checks (i.e. string or character comparisons) in order to see
     * if a required condition is met. This default implementation does always return {@code true}.
     *
     * @param line
     *         the line to inspect
     *
     * @return {@code true} if the line should be handed over to the regular expression scanner, {@code false} if the
     *         line does not contain a warning.
     */
{'fraction_non_alphanumeric': 0.08568612265014433, 'fraction_numerical': 0.003942828979793002, 'mean_word_length': 2.4993840847499382, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4261567', 'n_tokens_mistral': 3623, 'n_tokens_neox': 3437, 'n_words': 1273}