Intuitive DSL for Java

Version 2.0.0 · src/main/java/ch/dbalabs/intuitivedsl/exception/DslSyntaxException.java

Git clone
git clone https://www.dbalabs.ch/git/intuitive-dsl-java.git

DslSyntaxException.java

/*
 * 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();
    }
}