in gui/frontend/src/parsing/mysql/MySQLErrorListener.ts [88:323]
public override syntaxError<S extends Token, T extends ATNSimulator>(recognizer: Recognizer<T>,
offendingSymbol: S | null, line: number, charPositionInLine: number, msg: string,
e: RecognitionException | null): void {
let message = "";
// If not undefined then offendingSymbol is of type Token and the recognizer is of type Parser.
if (offendingSymbol) {
let token = offendingSymbol as Token;
if (!(recognizer instanceof Parser)) {
throw new Error("Unexpected type of recognizer in MySQLErrorListener.syntaxError()");
}
const parser = recognizer as MySQLMRSParser;
const lexer = parser.inputStream.tokenSource as MySQLBaseLexer;
const isEof = token.type === Token.EOF;
if (isEof) {
token = parser.inputStream.get(token.tokenIndex - 1);
}
const errorLength = token.stop - token.start + 1;
let wrongText = token.text || "";
// getExpectedTokens() ignores predicates, so it might include the token for which we got this syntax error,
// if that was excluded by a predicate (which in our case is always a version check).
// That's a good indicator to tell the user that this keyword is not valid *for the current server version*.
let expected = parser.getExpectedTokens();
let invalidForVersion = false;
let tokenType = token.type;
if (tokenType !== MySQLMRSLexer.IDENTIFIER && expected.contains(tokenType)) {
invalidForVersion = true;
} else {
tokenType = lexer.keywordFromText(wrongText);
if (expected.contains(tokenType)) {
invalidForVersion = true;
}
}
if (invalidForVersion) {
// The expected tokens set is read-only, so make a copy.
expected = new IntervalSet(expected);
expected.removeOne(tokenType);
}
// Try to find the expected input by examining the current parser context and
// the expected interval set. The latter often lists useless keywords, especially if they are allowed
// as identifiers.
let expectedText = "";
// Walk up from generic rules to reach something that gives us more context, if needed.
let context = parser.context!;
while (MySQLErrorListener.simpleRules.has(context.ruleIndex) && context.parent) {
context = context.parent;
}
switch (context.ruleIndex) {
case MySQLMRSParser.RULE_functionCall:
expectedText = "a complete function call or other expression";
break;
case MySQLMRSParser.RULE_expr:
expectedText = "an expression";
break;
case MySQLMRSParser.RULE_columnName:
case MySQLMRSParser.RULE_indexName:
case MySQLMRSParser.RULE_schemaName:
case MySQLMRSParser.RULE_procedureName:
case MySQLMRSParser.RULE_functionName:
case MySQLMRSParser.RULE_triggerName:
case MySQLMRSParser.RULE_viewName:
case MySQLMRSParser.RULE_tablespaceName:
case MySQLMRSParser.RULE_logfileGroupName:
case MySQLMRSParser.RULE_eventName:
case MySQLMRSParser.RULE_udfName:
case MySQLMRSParser.RULE_serverName:
case MySQLMRSParser.RULE_tableName:
case MySQLMRSParser.RULE_parameterName:
case MySQLMRSParser.RULE_labelIdentifier:
case MySQLMRSParser.RULE_roleIdentifier:
case MySQLMRSParser.RULE_windowName: {
const name = MySQLErrorListener.objectNames.get(context.ruleIndex);
if (!name) {
expectedText = "a new object name";
} else {
expectedText = `a new ${name} name`;
}
break;
}
case MySQLMRSParser.RULE_columnRef:
case MySQLMRSParser.RULE_indexRef:
case MySQLMRSParser.RULE_schemaRef:
case MySQLMRSParser.RULE_procedureRef:
case MySQLMRSParser.RULE_functionRef:
case MySQLMRSParser.RULE_triggerRef:
case MySQLMRSParser.RULE_viewRef:
case MySQLMRSParser.RULE_tablespaceRef:
case MySQLMRSParser.RULE_logfileGroupRef:
case MySQLMRSParser.RULE_eventRef:
case MySQLMRSParser.RULE_serverRef:
case MySQLMRSParser.RULE_engineRef:
case MySQLMRSParser.RULE_tableRef:
case MySQLMRSParser.RULE_filterTableRef:
case MySQLMRSParser.RULE_tableRefWithWildcard:
case MySQLMRSParser.RULE_labelRef:
case MySQLMRSParser.RULE_pluginRef:
case MySQLMRSParser.RULE_componentRef:
case MySQLMRSParser.RULE_resourceGroupRef: {
const name = MySQLErrorListener.objectNames.get(context.ruleIndex);
if (!name) {
expectedText = "the name of an existing object";
} else {
expectedText = "the name of an existing " + name;
}
break;
}
case MySQLMRSParser.RULE_columnInternalRef: {
expectedText = "a column name from this table";
break;
}
default: {
// If the expected set contains the IDENTIFIER token we likely want an identifier at this position.
// Due to the fact that MySQL defines a number of keywords as possible identifiers, we get all those
// whenever an identifier is actually required, bloating so the expected set with irrelevant
// elements. Hence we check for the identifier entry and assume we *only* want an identifier.
// This gives an imprecise result if both certain keywords *and* an identifier are expected.
if (expected.contains(MySQLMRSLexer.IDENTIFIER)) {
expectedText = "an identifier";
} else {
expectedText = this.intervalToString(expected, 6, parser.vocabulary);
}
break;
}
}
if (!wrongText.startsWith("\"") && !wrongText.startsWith("'") && !wrongText.startsWith("`")) {
wrongText = "\"" + wrongText + "\"";
}
if (!e) {
// Missing or unwanted tokens.
const oneOf = expectedText.length > 1 ? "one of " : "";
if (msg.includes("missing")) {
message = `Missing ${oneOf}${expectedText} before ${wrongText}`;
} else {
message = `Extraneous input ${wrongText} found, expecting ${oneOf}${expectedText}`;
}
} else {
if (e instanceof InputMismatchException) {
if (isEof) {
message = "Statement is incomplete";
} else {
message = wrongText + " is not valid at this position";
if (expectedText.length > 0) {
message += ", expecting " + expectedText;
}
}
} else if (e instanceof FailedPredicateException) {
// For cases like "... | a ({condition}? b)", but not "... | a ({condition}? b)?".
// Remove parts of the message we don't want.
const condition = e.message.substring("failed predicate: ".length);
condition.replace(/serverVersion/g, "server version");
condition.replace(/ && /g, "and");
message = wrongText + " is valid only for " + condition;
} if (e instanceof NoViableAltException) {
if (isEof) {
message = "Statement is incomplete";
} else {
message = wrongText + " is not valid at this position";
if (invalidForVersion) {
message += " for this server version";
}
}
if (expectedText.length > 0) {
message += ", expecting " + expectedText;
}
}
}
this.callback(message, token.type, token.start, line, charPositionInLine, errorLength);
} else {
// No offending symbol, which indicates this is a lexer error.
if (e instanceof NoViableAltException) {
if (!(recognizer instanceof Lexer)) {
throw new Error("Unexpected type of recognizer in MySQLErrorListener.syntaxError()");
}
const lexer = recognizer as MySQLMRSLexer;
const input = lexer.inputStream;
let text = lexer.getErrorDisplay(input.getTextFromRange(lexer.tokenStartCharIndex, input.index));
if (text === "") {
text = " "; // Should never happen, but we must ensure we have text.
}
switch (text[0]) {
case "/":
message = "Unfinished multiline comment";
break;
case '"':
message = "Unfinished double quoted string literal";
break;
case "'":
message = "Unfinished single quoted string literal";
break;
case "`":
message = "Unfinished back tick quoted string literal";
break;
default:
// Hex or bin string?
if (text.length > 1 && text[1] === "'" && (text.startsWith("x") || text.startsWith("b"))) {
message = "Unfinished " + (text.startsWith("x") ? "hex" : "binary") + " string literal";
break;
}
// Something else the lexer couldn't make sense of.
// Likely there is no rule that accepts this input.
message = "\"" + text + "\" is no valid input at all";
break;
}
this.callback(message, 0, lexer.tokenStartCharIndex, line, charPositionInLine,
input.index - lexer.tokenStartCharIndex);
}
}
}