LexerException.java
/*
* Copyright (c) 2015-2020, Stein Eldar Johnsen
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.lexer;
import net.morimekta.strings.ConsoleUtil;
import net.morimekta.strings.Displayable;
import java.io.IOException;
import static java.util.Objects.requireNonNull;
import static net.morimekta.strings.ConsoleUtil.isConsolePrintable;
import static net.morimekta.strings.ConsoleUtil.replaceNonPrintable;
/**
* Exception representing problems parsing tokens or other problems with
* returned tokens from the {@link Lexer}, or how a token interacts with
* other in the whole structure. This exception is not meant to represent
* problems that solely comes from the interaction between tokens in an
* otherwise sound document, e.g. where the problem can not be pinned
* to a single token causing the problem.
*/
public class LexerException extends IOException implements Displayable {
private static final long serialVersionUID = 883627013596058366L;
private CharSequence line;
private int lineNo = -1;
private int linePos = -1;
private int length = 0;
private Token<?> token;
/**
* Make exception cause by secondary problems.
*
* @param message Exception message.
*/
protected LexerException(String message) {
super(requireNonNull(message));
}
/**
* Make exception cause by secondary problems.
*
* @param cause The cause of the exception.
* @param message Exception message.
*/
protected LexerException(Throwable cause, String message) {
super(requireNonNull(message, "message == null"), requireNonNull(cause, "cause == null"));
if (cause instanceof LexerException) {
LexerException e = (LexerException) cause;
if (e.token != null) {
this.token = e.token;
} else {
this.lineNo = e.getLineNo();
this.linePos = e.getLinePos();
this.length = e.getLength();
this.line = e.getLine();
}
}
}
/**
* Make exception representing problems with a parsed token.
*
* @param token The problematic token.
* @param message The exception message.
*/
public LexerException(Token<?> token, String message) {
super(requireNonNull(message, "message == null"));
this.token = requireNonNull(token, "token == null");
}
/**
* Make exception representing a tokenizing problem at a specific position
* in the input.
*
* @param line The line the problem is located.
* @param lineNo The line number where the problem is (1-N).
* @param linePos The position in the line where the problem is (0-N).
* @param length Length of the error.
* @param message The exception message.
*/
public LexerException(CharSequence line, int lineNo, int linePos, int length, String message) {
super(requireNonNull(message, "message == null"));
this.line = requireNonNull(line, "line == null");
this.length = length;
this.lineNo = lineNo;
this.linePos = linePos;
}
@Override
public LexerException initCause(Throwable cause) {
super.initCause(cause);
return this;
}
/**
* @return The line string where the problem occurred.
*/
public CharSequence getLine() {
if (token != null) {
return token.line();
}
return line;
}
/**
* @return The line number where the problem occurred.
*/
public int getLineNo() {
if (token != null) {
return token.lineNo();
}
return lineNo;
}
/**
* @return The line position where the problem occurred.
*/
public int getLinePos() {
if (token != null) {
return token.linePos();
}
return linePos;
}
/**
* @return Length of match where fault lies.
*/
public int getLength() {
if (token != null) {
return token.length();
}
return length;
}
@Override
public String toString() {
return getClass().getSimpleName() + "\n" + displayString();
}
/**
* Replace non-printable chars in a string with something else. The
* replacement is static and only meant as a place-holder. It is advised
* to use a non-standard char as the replacement, as otherwise it will
* not be distinguishable from the standard "printable".
*
* @param str The string to escape.
* @param replacement Char to replace non-printable with.
* @return The escaped char string.
* @deprecated Use {@link ConsoleUtil#replaceNonPrintable(CharSequence, char)} in
* <code>utils-string</code> instead.
*/
@Deprecated(forRemoval = true)
public static String replaceNonPrintable(CharSequence str, char replacement) {
return ConsoleUtil.replaceNonPrintable(str, replacement);
}
/**
* @return Get the initial error string of the exception message. Defaults to 'Error'.
*/
protected String getError() {
return "Error";
}
@Override
public String displayString() {
if (token != null) {
return toStringInternal(
getError(),
token.lineNo(),
token.linePos(),
token.length(),
getMessage(),
token.line());
} else if (line != null) {
return toStringInternal(
getError(),
lineNo,
linePos,
length,
getMessage(),
line);
} else {
return String.format("%s: %s", getError(), getMessage());
}
}
/**
* Generate the toString of the exception based on various parts.
*
* @param error The initial 'error' string.
* @param lineNo The line number (1 indexed).
* @param linePos The line position (0 indexed).
* @param tokenLen The token length (in characters).
* @param message The error message.
* @param line The full line text.
* @return The formatted exception string.
*/
protected static String toStringInternal(
final String error,
final int lineNo,
final int linePos,
final int tokenLen,
final String message,
CharSequence line) {
int start = linePos;
int end = start + tokenLen;
if (line.length() > 120 && tokenLen < 100) {
if (end > 115) {
// cut at least 10 characters at beginning....
int remove = (end - 105);
start -= remove;
line = "..." + line.subSequence(remove + 3, line.length());
}
if (line.length() > 120) {
line = line.subSequence(0, 117) + "...";
}
}
if (tokenLen > 1) {
return String.format(
"%s on line %d row %d-%d: %s%n" +
"%s%n" +
"%s%s",
error,
lineNo,
linePos,
linePos + tokenLen - 1,
message,
ConsoleUtil.replaceNonPrintable(line, '·'),
"-".repeat(start - 1),
"^".repeat(tokenLen));
} else {
return String.format(
"%s on line %d row %d: %s%n" +
"%s%n" +
"%s^",
error,
lineNo,
linePos,
message,
ConsoleUtil.replaceNonPrintable(line, '·'),
"-".repeat(start - 1));
}
}
}