001/*
002 * Copyright (C) 2009-2011 Mathias Doenitz
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.parboiled.errors;
018
019import org.parboiled.buffers.InputBuffer;
020import org.parboiled.common.Formatter;
021import org.parboiled.common.StringUtils;
022import org.parboiled.matchers.Matcher;
023import org.parboiled.matchers.TestNotMatcher;
024import org.parboiled.support.MatcherPath;
025import org.parboiled.support.ParsingResult;
026import org.parboiled.support.Position;
027import org.parboiled.common.Formatter;
028import org.parboiled.common.Preconditions;
029import org.parboiled.common.StringUtils;
030import org.parboiled.matchers.Matcher;
031import org.parboiled.matchers.TestNotMatcher;
032import org.parboiled.support.MatcherPath;
033import org.parboiled.support.ParsingResult;
034import org.parboiled.support.Position;
035
036import java.util.List;
037
038import static org.parboiled.common.Preconditions.checkArgNotNull;
039import static org.parboiled.common.Preconditions.checkArgument;
040
041/**
042 * General utility methods regarding parse errors.
043 */
044public final class ErrorUtils {
045
046    private ErrorUtils() {}
047
048    /**
049     * Finds the Matcher in the given failedMatcherPath whose label is best for presentation in "expected" strings
050     * of parse error messages, given the provided lastMatchPath.
051     *
052     * @param path       the path to the failed matcher
053     * @param errorIndex the start index of the respective parse error
054     * @return the matcher whose label is best for presentation in "expected" strings
055     */
056    static Matcher findProperLabelMatcher(MatcherPath path, int errorIndex) {
057        try { return findProperLabelMatcher0(path, errorIndex); }
058        catch(RuntimeException e) {
059            if (e == UnderneathTestNot) return null; else throw e;
060        }
061    }
062
063    private static RuntimeException UnderneathTestNot = new RuntimeException() {
064        @Override public synchronized Throwable fillInStackTrace() { return this; }
065    };
066    private static Matcher findProperLabelMatcher0(MatcherPath path, int errorIndex) {
067        Preconditions.checkArgNotNull(path, "path");
068        Matcher found = path.parent != null ? findProperLabelMatcher0(path.parent, errorIndex) : null;
069        if (found != null) return found;
070        Matcher m = path.element.matcher;
071        if (m instanceof TestNotMatcher) throw UnderneathTestNot;
072        if (path.element.startIndex == errorIndex && m.hasCustomLabel()) return m;
073        return null;
074    }
075
076    /**
077     * Pretty prints the parse errors of the given ParsingResult showing their location in the given input buffer.
078     *
079     * @param parsingResult the parsing result
080     * @return the pretty print text
081     */
082    public static String printParseErrors(ParsingResult<?> parsingResult) {
083        Preconditions.checkArgNotNull(parsingResult, "parsingResult");
084        return printParseErrors(parsingResult.parseErrors);
085    }
086
087    /**
088     * Pretty prints the given parse errors showing their location in the given input buffer.
089     *
090     * @param errors      the parse errors
091     * @return the pretty print text
092     */
093    public static String printParseErrors(List<ParseError> errors) {
094        Preconditions.checkArgNotNull(errors, "errors");
095        StringBuilder sb = new StringBuilder();
096        for (ParseError error : errors) {
097            if (sb.length() > 0) sb.append("---\n");
098            sb.append(printParseError(error));
099        }
100        return sb.toString();
101    }
102
103    /**
104     * Pretty prints the given parse error showing its location in the given input buffer.
105     *
106     * @param error       the parse error
107     * @return the pretty print text
108     */
109    public static String printParseError(ParseError error) {
110        Preconditions.checkArgNotNull(error, "error");
111        return printParseError(error, new DefaultInvalidInputErrorFormatter());
112    }
113
114    /**
115     * Pretty prints the given parse error showing its location in the given input buffer.
116     *
117     * @param error       the parse error
118     * @param formatter   the formatter for InvalidInputErrors
119     * @return the pretty print text
120     */
121    public static String printParseError(ParseError error, Formatter<InvalidInputError> formatter) {
122        Preconditions.checkArgNotNull(error, "error");
123        Preconditions.checkArgNotNull(formatter, "formatter");
124        String message = error.getErrorMessage() != null ? error.getErrorMessage() :
125                error instanceof InvalidInputError ?
126                        formatter.format((InvalidInputError) error) : error.getClass().getSimpleName();
127        return printErrorMessage("%s (line %s, pos %s):", message,
128                error.getStartIndex(), error.getEndIndex(), error.getInputBuffer());
129    }
130
131    /**
132     * Prints an error message showing a location in the given InputBuffer.
133     *
134     * @param format       the format string, must include three placeholders for a string
135     *                     (the error message) and two integers (the error line / column respectively)
136     * @param errorMessage the error message
137     * @param errorIndex   the error location as an index into the inputBuffer
138     * @param inputBuffer  the underlying InputBuffer
139     * @return the error message including the relevant line from the underlying input plus location indicator
140     */
141    public static String printErrorMessage(String format, String errorMessage, int errorIndex,
142                                           InputBuffer inputBuffer) {
143        Preconditions.checkArgNotNull(inputBuffer, "inputBuffer");
144        return printErrorMessage(format, errorMessage, errorIndex, errorIndex + 1, inputBuffer);
145    }
146
147    /**
148     * Prints an error message showing a location in the given InputBuffer.
149     *
150     * @param format       the format string, must include three placeholders for a string
151     *                     (the error message) and two integers (the error line / column respectively)
152     * @param errorMessage the error message
153     * @param startIndex   the start location of the error as an index into the inputBuffer
154     * @param endIndex     the end location of the error as an index into the inputBuffer
155     * @param inputBuffer  the underlying InputBuffer
156     * @return the error message including the relevant line from the underlying input plus location indicators
157     */
158    public static String printErrorMessage(String format, String errorMessage, int startIndex, int endIndex,
159                                           InputBuffer inputBuffer) {
160        Preconditions.checkArgNotNull(inputBuffer, "inputBuffer");
161        Preconditions.checkArgument(startIndex <= endIndex);
162        Position pos = inputBuffer.getPosition(startIndex);
163        StringBuilder sb = new StringBuilder(String.format(format, errorMessage, pos.line, pos.column));
164        sb.append('\n');
165
166        String line = inputBuffer.extractLine(pos.line);
167        sb.append(line);
168        sb.append('\n');
169
170        int charCount = Math.max(Math.min(endIndex - startIndex, StringUtils.length(line) - pos.column + 2), 1);
171        for (int i = 0; i < pos.column - 1; i++) sb.append(' ');
172        for (int i = 0; i < charCount; i++) sb.append('^');
173        sb.append("\n");
174
175        return sb.toString();
176    }
177}