Skip to content

improve errorListener msg #281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/locale/locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const zh_CN = {
stmtInComplete: '语句不完整',
noValidPosition: '在此位置无效',
expecting: ',期望',
unfinishedMultilineComment: '未完成的多行注释',
unfinishedDoubleQuoted: '未完成的双引号字符串字变量',
unfinishedSingleQuoted: '未完成的单引号字符串字变量',
unfinishedTickQuoted: '未完成的反引号引用字符串字变量',
noValidInput: '没有有效的输入',
newObj: '一个新的对象',
existingObj: '一个存在的对象',
new: '一个新的',
existing: '一个存在的',
orKeyword: '或者一个关键字',
keyword: '一个关键字',
missing: '缺少',
at: '在',
or: '或者',
};

const en_US: typeof zh_CN = {
stmtInComplete: 'Statement is incomplete',
noValidPosition: 'is not valid at this position',
expecting: ', expecting ',
unfinishedMultilineComment: 'Unfinished multiline comment',
unfinishedDoubleQuoted: 'Unfinished double quoted string literal',
unfinishedSingleQuoted: 'Unfinished single quoted string literal',
unfinishedTickQuoted: 'Unfinished back tick quoted string literal',
noValidInput: 'is no valid input at all',
newObj: 'a new object',
existingObj: 'an existing object',
new: 'a new ',
existing: 'an existing ',
orKeyword: ' or a keyword',
keyword: 'a keyword',
missing: 'missing ',
at: ' at ',
or: ' or ',
};

const i18n = {
zh_CN,
en_US,
};

export { i18n };
20 changes: 14 additions & 6 deletions src/parser/common/basicSQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import {
ParseTreeWalker,
ParseTreeListener,
PredictionMode,
ANTLRErrorListener,
} from 'antlr4ng';
import { CandidatesCollection, CodeCompletionCore } from 'antlr4-c3';
import { SQLParserBase } from '../../lib/SQLParserBase';
import { findCaretTokenIndex } from './findCaretTokenIndex';
import { ctxToText, tokenToWord, WordRange, TextSlice } from './textAndWord';
import { CaretPosition, Suggestions, SyntaxSuggestion } from './types';
import { ParseError, ErrorListener, ParseErrorListener } from './parseErrorListener';
import { CaretPosition, LOCALE_TYPE, Suggestions, SyntaxSuggestion } from './types';
import { ParseError, ErrorListener } from './parseErrorListener';
import { ErrorStrategy } from './errorStrategy';
import type { SplitListener } from './splitListener';
import type { EntityCollector } from './entityCollector';
Expand Down Expand Up @@ -78,6 +79,11 @@ export abstract class BasicSQL<
*/
protected abstract get splitListener(): SplitListener<ParserRuleContext>;

/**
* Get a new errorListener instance.
*/
protected abstract createErrorListener(errorListener: ErrorListener): ANTLRErrorListener;

/**
* Get a new entityCollector instance.
*/
Expand All @@ -86,6 +92,8 @@ export abstract class BasicSQL<
caretTokenIndex?: number
): EntityCollector;

public locale: LOCALE_TYPE = 'en_US';

/**
* Create an antlr4 lexer from input.
* @param input string
Expand All @@ -95,7 +103,7 @@ export abstract class BasicSQL<
const lexer = this.createLexerFromCharStream(charStreams);
if (errorListener) {
lexer.removeErrorListeners();
lexer.addErrorListener(new ParseErrorListener(errorListener));
lexer.addErrorListener(this.createErrorListener(errorListener));
}
return lexer;
}
Expand All @@ -111,7 +119,7 @@ export abstract class BasicSQL<
parser.interpreter.predictionMode = PredictionMode.SLL;
if (errorListener) {
parser.removeErrorListeners();
parser.addErrorListener(new ParseErrorListener(errorListener));
parser.addErrorListener(this.createErrorListener(errorListener));
}

return parser;
Expand Down Expand Up @@ -142,7 +150,7 @@ export abstract class BasicSQL<
this._lexer = this.createLexerFromCharStream(this._charStreams);

this._lexer.removeErrorListeners();
this._lexer.addErrorListener(new ParseErrorListener(this._errorListener));
this._lexer.addErrorListener(this.createErrorListener(this._errorListener));

this._tokenStream = new CommonTokenStream(this._lexer);
/**
Expand Down Expand Up @@ -178,7 +186,7 @@ export abstract class BasicSQL<
this._parsedInput = input;

parser.removeErrorListeners();
parser.addErrorListener(new ParseErrorListener(this._errorListener));
parser.addErrorListener(this.createErrorListener(this._errorListener));

this._parseTree = parser.program();

Expand Down
98 changes: 95 additions & 3 deletions src/parser/common/parseErrorListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import {
ANTLRErrorListener,
RecognitionException,
ATNSimulator,
LexerNoViableAltException,
Lexer,
Parser,
InputMismatchException,
NoViableAltException,
} from 'antlr4ng';
import { LOCALE_TYPE } from './types';
import { transform } from './transform';

/**
* Converted from {@link SyntaxError}.
Expand Down Expand Up @@ -39,10 +46,12 @@ export interface SyntaxError {
*/
export type ErrorListener = (parseError: ParseError, originalError: SyntaxError) => void;

export class ParseErrorListener implements ANTLRErrorListener {
export abstract class ParseErrorListener implements ANTLRErrorListener {
private _errorListener: ErrorListener;
private locale: LOCALE_TYPE;

constructor(errorListener: ErrorListener) {
constructor(errorListener: ErrorListener, locale: LOCALE_TYPE = 'en_US') {
this.locale = locale;
this._errorListener = errorListener;
}

Expand All @@ -52,6 +61,8 @@ export class ParseErrorListener implements ANTLRErrorListener {

reportContextSensitivity() {}

protected abstract getExpectedText(parser: Parser, token: Token): string;

syntaxError(
recognizer: Recognizer<ATNSimulator>,
offendingSymbol: Token | null,
Expand All @@ -60,6 +71,87 @@ export class ParseErrorListener implements ANTLRErrorListener {
msg: string,
e: RecognitionException
) {
let message = '';
// If not undefined then offendingSymbol is of type Token.
if (offendingSymbol) {
let token = offendingSymbol as Token;
const parser = recognizer as Parser;

// judge token is EOF
const isEof = token.type === Token.EOF;
if (isEof) {
token = parser.tokenStream.get(token.tokenIndex - 1);
}
const wrongText = token.text ?? '';

const isInComplete = isEof && wrongText !== ' ';

const expectedText = isInComplete ? '' : this.getExpectedText(parser, token);

if (!e) {
// handle missing or unwanted tokens.
message = msg;
if (msg.includes('extraneous')) {
message = `'${wrongText}' {noValidPosition}${
expectedText.length ? `{expecting}${expectedText}` : ''
}`;
}
if (msg.includes('missing')) {
const regex = /missing\s+'([^']+)'/;
const match = msg.match(regex);
message = `{missing}`;
if (match) {
const missKeyword = match[1];
message += `'${missKeyword}'`;
} else {
message += `{keyword}`;
}
message += `{at}'${wrongText}'`;
}
} else {
// handle mismatch exception or no viable alt exception
if (e instanceof InputMismatchException || e instanceof NoViableAltException) {
if (isEof) {
message = `{stmtInComplete}`;
} else {
message = `'${wrongText}' {noValidPosition}`;
}
if (expectedText.length > 0) {
message += `{expecting}${expectedText}`;
}
} else {
message = msg;
}
}
} else {
// No offending symbol, which indicates this is a lexer error.
if (e instanceof LexerNoViableAltException) {
const lexer = recognizer as Lexer;
const input = lexer.inputStream;
let text = lexer.getErrorDisplay(
input.getText(lexer._tokenStartCharIndex, input.index)
);
switch (text[0]) {
case '/':
message = '{unfinishedMultilineComment}';
break;
case '"':
message = '{unfinishedDoubleQuoted}';
break;
case "'":
message = '{unfinishedSingleQuoted}';
break;
case '`':
message = '{unfinishedTickQuoted}';
break;

default:
message = '"' + text + '" {noValidInput}';
break;
}
}
}
message = transform(message, this.locale);
let endCol = charPositionInLine + 1;
if (offendingSymbol && offendingSymbol.text !== null) {
endCol = charPositionInLine + offendingSymbol.text.length;
Expand All @@ -71,7 +163,7 @@ export class ParseErrorListener implements ANTLRErrorListener {
endLine: line,
startColumn: charPositionInLine + 1,
endColumn: endCol + 1,
message: msg,
message,
},
{
e,
Expand Down
17 changes: 17 additions & 0 deletions src/parser/common/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { LOCALE_TYPE } from './types';
import { i18n } from '../../locale/locale';

/**
* transform message to locale language
* @param message error msg
* @param locale language setting
*/
function transform(message: string, locale: LOCALE_TYPE) {
const regex = /{([^}]+)}/g;
return message.replace(
regex,
(_, key: keyof (typeof i18n)[typeof locale]) => i18n[locale][key] || ''
);
}

export { transform };
2 changes: 2 additions & 0 deletions src/parser/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ export interface Suggestions<T = WordRange> {
*/
readonly keywords: string[];
}

export type LOCALE_TYPE = 'zh_CN' | 'en_US';
76 changes: 76 additions & 0 deletions src/parser/flink/flinkErrorListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { CodeCompletionCore } from 'antlr4-c3';
import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener';
import { Parser, Token } from 'antlr4ng';
import { FlinkSqlParser } from '../../lib/flink/FlinkSqlParser';
import { LOCALE_TYPE } from '../common/types';

export class FlinkErrorListener extends ParseErrorListener {
private preferredRules: Set<number>;

private objectNames: Map<number, string> = new Map([
[FlinkSqlParser.RULE_catalogPath, 'catalog'],
[FlinkSqlParser.RULE_catalogPathCreate, 'catalog'],
[FlinkSqlParser.RULE_databasePath, 'database'],
[FlinkSqlParser.RULE_databasePathCreate, 'database'],
[FlinkSqlParser.RULE_tablePath, 'table'],
[FlinkSqlParser.RULE_tablePathCreate, 'table'],
[FlinkSqlParser.RULE_viewPath, 'view'],
[FlinkSqlParser.RULE_viewPathCreate, 'view'],
[FlinkSqlParser.RULE_functionName, 'function'],
[FlinkSqlParser.RULE_functionNameCreate, 'function'],
[FlinkSqlParser.RULE_columnName, 'column'],
[FlinkSqlParser.RULE_columnNameCreate, 'column'],
]);

constructor(errorListener: ErrorListener, preferredRules: Set<number>, locale: LOCALE_TYPE) {
super(errorListener, locale);
this.preferredRules = preferredRules;
}

public getExpectedText(parser: Parser, token: Token) {
let expectedText = '';

let currentContext = parser.context ?? undefined;
while (currentContext?.parent) {
currentContext = currentContext.parent;
}

const core = new CodeCompletionCore(parser);
core.preferredRules = this.preferredRules;
const candidates = core.collectCandidates(token.tokenIndex, currentContext);

if (candidates.rules.size) {
const result: string[] = [];
// get expectedText as collect rules first
for (const candidate of candidates.rules) {
const [ruleType] = candidate;
const name = this.objectNames.get(ruleType);
switch (ruleType) {
case FlinkSqlParser.RULE_databasePath:
case FlinkSqlParser.RULE_tablePath:
case FlinkSqlParser.RULE_viewPath:
case FlinkSqlParser.RULE_functionName:
case FlinkSqlParser.RULE_columnName:
case FlinkSqlParser.RULE_catalogPath: {
result.push(`{existing}${name}`);
break;
}
case FlinkSqlParser.RULE_databasePathCreate:
case FlinkSqlParser.RULE_tablePathCreate:
case FlinkSqlParser.RULE_functionNameCreate:
case FlinkSqlParser.RULE_viewPathCreate:
case FlinkSqlParser.RULE_columnNameCreate:
case FlinkSqlParser.RULE_catalogPathCreate: {
result.push(`{new}${name}`);
break;
}
}
}
expectedText = result.join('{or}');
}
if (candidates.tokens.size) {
expectedText += expectedText ? '{orKeyword}' : '{keyword}';
}
return expectedText;
}
}
6 changes: 6 additions & 0 deletions src/parser/flink/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { BasicSQL } from '../common/basicSQL';
import { StmtContextType } from '../common/entityCollector';
import { FlinkSqlSplitListener } from './flinkSplitListener';
import { FlinkEntityCollector } from './flinkEntityCollector';
import { ErrorListener } from '../common/parseErrorListener';
import { FlinkErrorListener } from './flinkErrorListener';

export { FlinkSqlSplitListener, FlinkEntityCollector };

Expand Down Expand Up @@ -37,6 +39,10 @@ export class FlinkSQL extends BasicSQL<FlinkSqlLexer, ProgramContext, FlinkSqlPa
return new FlinkSqlSplitListener();
}

protected createErrorListener(_errorListener: ErrorListener) {
return new FlinkErrorListener(_errorListener, this.preferredRules, this.locale);
}

protected createEntityCollector(input: string, caretTokenIndex?: number) {
return new FlinkEntityCollector(input, caretTokenIndex);
}
Expand Down
Loading
Loading