/*
* This file is part of the Intuitive DSL project.
* Copyright (c) 2026 DBA Labs - Switzerland. All rights reserved.
*
* This program is dual-licensed under a commercial license and the AGPLv3.
* For commercial licensing, contact us at [email protected] or visit https://www.dbalabs.ch.
*
* AGPLv3 licensing:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 (19 November 2007).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
package ch.dbalabs.intuitivedsl.exception;
import ch.dbalabs.intuitivedsl.parser.Token;
import java.util.List;
/**
* Exception thrown at runtime during the parsing of the user input
* if the provided string does not match the expected AST (Abstract Syntax Tree).
*
* This exception holds contextual information (unexpected token and expected possibilities)
* to build rich, compiler-style error messages for the end user.
*
* @author DBA Labs
*/
public class DslSyntaxException extends RuntimeException {
private final String input;
private final transient Token unexpectedToken;
private final List<String> expectedPossibilities;
/**
* Constructs a new DslSyntaxException.
*
* @param input the raw input string provided by the user
* @param unexpectedToken the token that caused the parsing to fail.
* For premature end-of-input situations, this may be the synthetic EOF token
* produced by the lexer, or {@code null} if no token object is available.
* @param expectedPossibilities the list of valid tokens/keywords expected at this position
*/
public DslSyntaxException(String input, Token unexpectedToken, List<String> expectedPossibilities) {
super("Syntax error: unexpected token '" + (unexpectedToken != null ? unexpectedToken.value() : "EOF") + "'");
this.input = input;
this.unexpectedToken = unexpectedToken;
this.expectedPossibilities = expectedPossibilities;
}
/**
* Retrieves the token that caused the parsing failure.
*
* @return the unexpected token, possibly the synthetic EOF token, or {@code null}
* if the parser had no token object available for the failure point
*/
public Token getUnexpectedToken() {
return unexpectedToken;
}
/**
* Retrieves the list of expected valid symbols at the point of failure.
*
* @return a list of expected string representations
*/
public List<String> getExpectedPossibilities() {
return expectedPossibilities;
}
/**
* Generates an enriched, compiler-style error message for a perfect terminal UX.
*
* Example output:
* <pre>
* Syntax Error at col 25: Unexpected token 'FRO'.
* Input: SHOW COMMAND HISTORY u1 FRO
* ^^^
* Expected: { LAST | FROM | COMMAND | ; }
* </pre>
*
* @return the formatted error message
*/
public String getRichMessage() {
StringBuilder sb = new StringBuilder();
int pos = (unexpectedToken != null) ? unexpectedToken.position() : input.length();
String tokenVal = (unexpectedToken != null) ? unexpectedToken.value() : "EOF";
sb.append("Syntax Error at col ").append(pos)
.append(": Unexpected token '").append(tokenVal).append("'.\n");
sb.append("Input: ").append(input).append("\n");
// Align the cursor (^) under the faulty token
sb.append(" "); // Margin matching the length of "Input: "
sb.append(" ".repeat(Math.max(0, pos)));
int length = tokenVal.equals("EOF") ? 1 : tokenVal.length();
sb.append("^".repeat(length)).append("\n");
if (expectedPossibilities != null && !expectedPossibilities.isEmpty()) {
sb.append("Expected: { ")
.append(String.join(" | ", expectedPossibilities))
.append(" }");
}
return sb.toString();
}
}