Skip to content

Commit 4b7f054

Browse files
authored
feat: improve errorListener msg (#281)
* feat: add mysql errorListener and commonErrorListener * feat: improve other sql error msg * feat: support i18n for error msg * feat: add all sql errorMsg unit test * feat: update locale file and change i18n funtion name * test: upate error unit test
1 parent e3eb799 commit 4b7f054

27 files changed

+1310
-9
lines changed

src/locale/locale.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const zh_CN = {
2+
stmtInComplete: '语句不完整',
3+
noValidPosition: '在此位置无效',
4+
expecting: ',期望',
5+
unfinishedMultilineComment: '未完成的多行注释',
6+
unfinishedDoubleQuoted: '未完成的双引号字符串字变量',
7+
unfinishedSingleQuoted: '未完成的单引号字符串字变量',
8+
unfinishedTickQuoted: '未完成的反引号引用字符串字变量',
9+
noValidInput: '没有有效的输入',
10+
newObj: '一个新的对象',
11+
existingObj: '一个存在的对象',
12+
new: '一个新的',
13+
existing: '一个存在的',
14+
orKeyword: '或者一个关键字',
15+
keyword: '一个关键字',
16+
missing: '缺少',
17+
at: '在',
18+
or: '或者',
19+
};
20+
21+
const en_US: typeof zh_CN = {
22+
stmtInComplete: 'Statement is incomplete',
23+
noValidPosition: 'is not valid at this position',
24+
expecting: ', expecting ',
25+
unfinishedMultilineComment: 'Unfinished multiline comment',
26+
unfinishedDoubleQuoted: 'Unfinished double quoted string literal',
27+
unfinishedSingleQuoted: 'Unfinished single quoted string literal',
28+
unfinishedTickQuoted: 'Unfinished back tick quoted string literal',
29+
noValidInput: 'is no valid input at all',
30+
newObj: 'a new object',
31+
existingObj: 'an existing object',
32+
new: 'a new ',
33+
existing: 'an existing ',
34+
orKeyword: ' or a keyword',
35+
keyword: 'a keyword',
36+
missing: 'missing ',
37+
at: ' at ',
38+
or: ' or ',
39+
};
40+
41+
const i18n = {
42+
zh_CN,
43+
en_US,
44+
};
45+
46+
export { i18n };

src/parser/common/basicSQL.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import {
88
ParseTreeWalker,
99
ParseTreeListener,
1010
PredictionMode,
11+
ANTLRErrorListener,
1112
} from 'antlr4ng';
1213
import { CandidatesCollection, CodeCompletionCore } from 'antlr4-c3';
1314
import { SQLParserBase } from '../../lib/SQLParserBase';
1415
import { findCaretTokenIndex } from './findCaretTokenIndex';
1516
import { ctxToText, tokenToWord, WordRange, TextSlice } from './textAndWord';
16-
import { CaretPosition, Suggestions, SyntaxSuggestion } from './types';
17-
import { ParseError, ErrorListener, ParseErrorListener } from './parseErrorListener';
17+
import { CaretPosition, LOCALE_TYPE, Suggestions, SyntaxSuggestion } from './types';
18+
import { ParseError, ErrorListener } from './parseErrorListener';
1819
import { ErrorStrategy } from './errorStrategy';
1920
import type { SplitListener } from './splitListener';
2021
import type { EntityCollector } from './entityCollector';
@@ -78,6 +79,11 @@ export abstract class BasicSQL<
7879
*/
7980
protected abstract get splitListener(): SplitListener<ParserRuleContext>;
8081

82+
/**
83+
* Get a new errorListener instance.
84+
*/
85+
protected abstract createErrorListener(errorListener: ErrorListener): ANTLRErrorListener;
86+
8187
/**
8288
* Get a new entityCollector instance.
8389
*/
@@ -86,6 +92,8 @@ export abstract class BasicSQL<
8692
caretTokenIndex?: number
8793
): EntityCollector;
8894

95+
public locale: LOCALE_TYPE = 'en_US';
96+
8997
/**
9098
* Create an antlr4 lexer from input.
9199
* @param input string
@@ -95,7 +103,7 @@ export abstract class BasicSQL<
95103
const lexer = this.createLexerFromCharStream(charStreams);
96104
if (errorListener) {
97105
lexer.removeErrorListeners();
98-
lexer.addErrorListener(new ParseErrorListener(errorListener));
106+
lexer.addErrorListener(this.createErrorListener(errorListener));
99107
}
100108
return lexer;
101109
}
@@ -111,7 +119,7 @@ export abstract class BasicSQL<
111119
parser.interpreter.predictionMode = PredictionMode.SLL;
112120
if (errorListener) {
113121
parser.removeErrorListeners();
114-
parser.addErrorListener(new ParseErrorListener(errorListener));
122+
parser.addErrorListener(this.createErrorListener(errorListener));
115123
}
116124

117125
return parser;
@@ -142,7 +150,7 @@ export abstract class BasicSQL<
142150
this._lexer = this.createLexerFromCharStream(this._charStreams);
143151

144152
this._lexer.removeErrorListeners();
145-
this._lexer.addErrorListener(new ParseErrorListener(this._errorListener));
153+
this._lexer.addErrorListener(this.createErrorListener(this._errorListener));
146154

147155
this._tokenStream = new CommonTokenStream(this._lexer);
148156
/**
@@ -178,7 +186,7 @@ export abstract class BasicSQL<
178186
this._parsedInput = input;
179187

180188
parser.removeErrorListeners();
181-
parser.addErrorListener(new ParseErrorListener(this._errorListener));
189+
parser.addErrorListener(this.createErrorListener(this._errorListener));
182190

183191
this._parseTree = parser.program();
184192

src/parser/common/parseErrorListener.ts

+95-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import {
44
ANTLRErrorListener,
55
RecognitionException,
66
ATNSimulator,
7+
LexerNoViableAltException,
8+
Lexer,
9+
Parser,
10+
InputMismatchException,
11+
NoViableAltException,
712
} from 'antlr4ng';
13+
import { LOCALE_TYPE } from './types';
14+
import { transform } from './transform';
815

916
/**
1017
* Converted from {@link SyntaxError}.
@@ -39,10 +46,12 @@ export interface SyntaxError {
3946
*/
4047
export type ErrorListener = (parseError: ParseError, originalError: SyntaxError) => void;
4148

42-
export class ParseErrorListener implements ANTLRErrorListener {
49+
export abstract class ParseErrorListener implements ANTLRErrorListener {
4350
private _errorListener: ErrorListener;
51+
private locale: LOCALE_TYPE;
4452

45-
constructor(errorListener: ErrorListener) {
53+
constructor(errorListener: ErrorListener, locale: LOCALE_TYPE = 'en_US') {
54+
this.locale = locale;
4655
this._errorListener = errorListener;
4756
}
4857

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

5362
reportContextSensitivity() {}
5463

64+
protected abstract getExpectedText(parser: Parser, token: Token): string;
65+
5566
syntaxError(
5667
recognizer: Recognizer<ATNSimulator>,
5768
offendingSymbol: Token | null,
@@ -60,6 +71,87 @@ export class ParseErrorListener implements ANTLRErrorListener {
6071
msg: string,
6172
e: RecognitionException
6273
) {
74+
let message = '';
75+
// If not undefined then offendingSymbol is of type Token.
76+
if (offendingSymbol) {
77+
let token = offendingSymbol as Token;
78+
const parser = recognizer as Parser;
79+
80+
// judge token is EOF
81+
const isEof = token.type === Token.EOF;
82+
if (isEof) {
83+
token = parser.tokenStream.get(token.tokenIndex - 1);
84+
}
85+
const wrongText = token.text ?? '';
86+
87+
const isInComplete = isEof && wrongText !== ' ';
88+
89+
const expectedText = isInComplete ? '' : this.getExpectedText(parser, token);
90+
91+
if (!e) {
92+
// handle missing or unwanted tokens.
93+
message = msg;
94+
if (msg.includes('extraneous')) {
95+
message = `'${wrongText}' {noValidPosition}${
96+
expectedText.length ? `{expecting}${expectedText}` : ''
97+
}`;
98+
}
99+
if (msg.includes('missing')) {
100+
const regex = /missing\s+'([^']+)'/;
101+
const match = msg.match(regex);
102+
message = `{missing}`;
103+
if (match) {
104+
const missKeyword = match[1];
105+
message += `'${missKeyword}'`;
106+
} else {
107+
message += `{keyword}`;
108+
}
109+
message += `{at}'${wrongText}'`;
110+
}
111+
} else {
112+
// handle mismatch exception or no viable alt exception
113+
if (e instanceof InputMismatchException || e instanceof NoViableAltException) {
114+
if (isEof) {
115+
message = `{stmtInComplete}`;
116+
} else {
117+
message = `'${wrongText}' {noValidPosition}`;
118+
}
119+
if (expectedText.length > 0) {
120+
message += `{expecting}${expectedText}`;
121+
}
122+
} else {
123+
message = msg;
124+
}
125+
}
126+
} else {
127+
// No offending symbol, which indicates this is a lexer error.
128+
if (e instanceof LexerNoViableAltException) {
129+
const lexer = recognizer as Lexer;
130+
const input = lexer.inputStream;
131+
let text = lexer.getErrorDisplay(
132+
input.getText(lexer._tokenStartCharIndex, input.index)
133+
);
134+
switch (text[0]) {
135+
case '/':
136+
message = '{unfinishedMultilineComment}';
137+
break;
138+
case '"':
139+
message = '{unfinishedDoubleQuoted}';
140+
break;
141+
case "'":
142+
message = '{unfinishedSingleQuoted}';
143+
break;
144+
case '`':
145+
message = '{unfinishedTickQuoted}';
146+
break;
147+
148+
default:
149+
message = '"' + text + '" {noValidInput}';
150+
break;
151+
}
152+
}
153+
}
154+
message = transform(message, this.locale);
63155
let endCol = charPositionInLine + 1;
64156
if (offendingSymbol && offendingSymbol.text !== null) {
65157
endCol = charPositionInLine + offendingSymbol.text.length;
@@ -71,7 +163,7 @@ export class ParseErrorListener implements ANTLRErrorListener {
71163
endLine: line,
72164
startColumn: charPositionInLine + 1,
73165
endColumn: endCol + 1,
74-
message: msg,
166+
message,
75167
},
76168
{
77169
e,

src/parser/common/transform.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { LOCALE_TYPE } from './types';
2+
import { i18n } from '../../locale/locale';
3+
4+
/**
5+
* transform message to locale language
6+
* @param message error msg
7+
* @param locale language setting
8+
*/
9+
function transform(message: string, locale: LOCALE_TYPE) {
10+
const regex = /{([^}]+)}/g;
11+
return message.replace(
12+
regex,
13+
(_, key: keyof (typeof i18n)[typeof locale]) => i18n[locale][key] || ''
14+
);
15+
}
16+
17+
export { transform };

src/parser/common/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,5 @@ export interface Suggestions<T = WordRange> {
6767
*/
6868
readonly keywords: string[];
6969
}
70+
71+
export type LOCALE_TYPE = 'zh_CN' | 'en_US';
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { CodeCompletionCore } from 'antlr4-c3';
2+
import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener';
3+
import { Parser, Token } from 'antlr4ng';
4+
import { FlinkSqlParser } from '../../lib/flink/FlinkSqlParser';
5+
import { LOCALE_TYPE } from '../common/types';
6+
7+
export class FlinkErrorListener extends ParseErrorListener {
8+
private preferredRules: Set<number>;
9+
10+
private objectNames: Map<number, string> = new Map([
11+
[FlinkSqlParser.RULE_catalogPath, 'catalog'],
12+
[FlinkSqlParser.RULE_catalogPathCreate, 'catalog'],
13+
[FlinkSqlParser.RULE_databasePath, 'database'],
14+
[FlinkSqlParser.RULE_databasePathCreate, 'database'],
15+
[FlinkSqlParser.RULE_tablePath, 'table'],
16+
[FlinkSqlParser.RULE_tablePathCreate, 'table'],
17+
[FlinkSqlParser.RULE_viewPath, 'view'],
18+
[FlinkSqlParser.RULE_viewPathCreate, 'view'],
19+
[FlinkSqlParser.RULE_functionName, 'function'],
20+
[FlinkSqlParser.RULE_functionNameCreate, 'function'],
21+
[FlinkSqlParser.RULE_columnName, 'column'],
22+
[FlinkSqlParser.RULE_columnNameCreate, 'column'],
23+
]);
24+
25+
constructor(errorListener: ErrorListener, preferredRules: Set<number>, locale: LOCALE_TYPE) {
26+
super(errorListener, locale);
27+
this.preferredRules = preferredRules;
28+
}
29+
30+
public getExpectedText(parser: Parser, token: Token) {
31+
let expectedText = '';
32+
33+
let currentContext = parser.context ?? undefined;
34+
while (currentContext?.parent) {
35+
currentContext = currentContext.parent;
36+
}
37+
38+
const core = new CodeCompletionCore(parser);
39+
core.preferredRules = this.preferredRules;
40+
const candidates = core.collectCandidates(token.tokenIndex, currentContext);
41+
42+
if (candidates.rules.size) {
43+
const result: string[] = [];
44+
// get expectedText as collect rules first
45+
for (const candidate of candidates.rules) {
46+
const [ruleType] = candidate;
47+
const name = this.objectNames.get(ruleType);
48+
switch (ruleType) {
49+
case FlinkSqlParser.RULE_databasePath:
50+
case FlinkSqlParser.RULE_tablePath:
51+
case FlinkSqlParser.RULE_viewPath:
52+
case FlinkSqlParser.RULE_functionName:
53+
case FlinkSqlParser.RULE_columnName:
54+
case FlinkSqlParser.RULE_catalogPath: {
55+
result.push(`{existing}${name}`);
56+
break;
57+
}
58+
case FlinkSqlParser.RULE_databasePathCreate:
59+
case FlinkSqlParser.RULE_tablePathCreate:
60+
case FlinkSqlParser.RULE_functionNameCreate:
61+
case FlinkSqlParser.RULE_viewPathCreate:
62+
case FlinkSqlParser.RULE_columnNameCreate:
63+
case FlinkSqlParser.RULE_catalogPathCreate: {
64+
result.push(`{new}${name}`);
65+
break;
66+
}
67+
}
68+
}
69+
expectedText = result.join('{or}');
70+
}
71+
if (candidates.tokens.size) {
72+
expectedText += expectedText ? '{orKeyword}' : '{keyword}';
73+
}
74+
return expectedText;
75+
}
76+
}

src/parser/flink/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { BasicSQL } from '../common/basicSQL';
77
import { StmtContextType } from '../common/entityCollector';
88
import { FlinkSqlSplitListener } from './flinkSplitListener';
99
import { FlinkEntityCollector } from './flinkEntityCollector';
10+
import { ErrorListener } from '../common/parseErrorListener';
11+
import { FlinkErrorListener } from './flinkErrorListener';
1012

1113
export { FlinkSqlSplitListener, FlinkEntityCollector };
1214

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

42+
protected createErrorListener(_errorListener: ErrorListener) {
43+
return new FlinkErrorListener(_errorListener, this.preferredRules, this.locale);
44+
}
45+
4046
protected createEntityCollector(input: string, caretTokenIndex?: number) {
4147
return new FlinkEntityCollector(input, caretTokenIndex);
4248
}

0 commit comments

Comments
 (0)