From 21b0fbb6e84f978a499e430c826dacc161e62d3d Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Thu, 28 Mar 2024 15:42:21 +0800 Subject: [PATCH 1/6] feat: add mysql errorListener and commonErrorListener --- src/parser/common/basicSQL.ts | 14 +++-- src/parser/common/parseErrorListener.ts | 79 ++++++++++++++++++++++++- src/parser/mysql/index.ts | 6 ++ src/parser/mysql/mysqlErrorListener.ts | 75 +++++++++++++++++++++++ test/parser/mysql/errorListener.test.ts | 48 +++++++++++++++ 5 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 src/parser/mysql/mysqlErrorListener.ts create mode 100644 test/parser/mysql/errorListener.test.ts diff --git a/src/parser/common/basicSQL.ts b/src/parser/common/basicSQL.ts index 31082f6d..047c4886 100644 --- a/src/parser/common/basicSQL.ts +++ b/src/parser/common/basicSQL.ts @@ -8,6 +8,7 @@ import { ParseTreeWalker, ParseTreeListener, PredictionMode, + ANTLRErrorListener, } from 'antlr4ng'; import { CandidatesCollection, CodeCompletionCore } from 'antlr4-c3'; import { SQLParserBase } from '../../lib/SQLParserBase'; @@ -78,6 +79,11 @@ export abstract class BasicSQL< */ protected abstract get splitListener(): SplitListener; + /** + * Get a new errorListener instance. + */ + protected abstract createErrorListener(errorListener: ErrorListener): ANTLRErrorListener; + /** * Get a new entityCollector instance. */ @@ -95,7 +101,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; } @@ -111,7 +117,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; @@ -142,7 +148,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); /** @@ -178,7 +184,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(); diff --git a/src/parser/common/parseErrorListener.ts b/src/parser/common/parseErrorListener.ts index 0a1295bc..f1f85a9f 100644 --- a/src/parser/common/parseErrorListener.ts +++ b/src/parser/common/parseErrorListener.ts @@ -4,6 +4,11 @@ import { ANTLRErrorListener, RecognitionException, ATNSimulator, + LexerNoViableAltException, + Lexer, + Parser, + InputMismatchException, + NoViableAltException, } from 'antlr4ng'; /** @@ -39,7 +44,7 @@ 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; constructor(errorListener: ErrorListener) { @@ -52,6 +57,8 @@ export class ParseErrorListener implements ANTLRErrorListener { reportContextSensitivity() {} + protected abstract getExpectedText(parser: Parser, token: Token): string; + syntaxError( recognizer: Recognizer, offendingSymbol: Token | null, @@ -60,6 +67,74 @@ 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}' is not valid at this position${ + expectedText.length ? `, expecting ${expectedText}` : '' + }`; + } + } else { + // handle mismatch exception or no viable alt exception + if (e instanceof InputMismatchException || e instanceof NoViableAltException) { + if (isEof) { + message = `statement is incomplete`; + } else { + message = `'${wrongText}' is not valid at this position`; + } + 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 = '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: + message = '"' + text + '" is no valid input at all'; + break; + } + } + } let endCol = charPositionInLine + 1; if (offendingSymbol && offendingSymbol.text !== null) { endCol = charPositionInLine + offendingSymbol.text.length; @@ -71,7 +146,7 @@ export class ParseErrorListener implements ANTLRErrorListener { endLine: line, startColumn: charPositionInLine + 1, endColumn: endCol + 1, - message: msg, + message: message, }, { e, diff --git a/src/parser/mysql/index.ts b/src/parser/mysql/index.ts index 098752ff..155ddc82 100644 --- a/src/parser/mysql/index.ts +++ b/src/parser/mysql/index.ts @@ -7,6 +7,8 @@ import { Suggestions, EntityContextType, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { MysqlSplitListener } from './mysqlSplitListener'; import { MySqlEntityCollector } from './mysqlEntityCollector'; +import { MysqlErrorListener } from './mysqlErrorListener'; +import { ErrorListener } from '../common/parseErrorListener'; export { MySqlEntityCollector, MysqlSplitListener }; @@ -36,6 +38,10 @@ export class MySQL extends BasicSQL { return new MysqlSplitListener(); } + protected createErrorListener(_errorListener: ErrorListener) { + return new MysqlErrorListener(_errorListener, this.preferredRules); + } + protected createEntityCollector(input: string, caretTokenIndex?: number) { return new MySqlEntityCollector(input, caretTokenIndex); } diff --git a/src/parser/mysql/mysqlErrorListener.ts b/src/parser/mysql/mysqlErrorListener.ts new file mode 100644 index 00000000..307eb510 --- /dev/null +++ b/src/parser/mysql/mysqlErrorListener.ts @@ -0,0 +1,75 @@ +import { CodeCompletionCore } from 'antlr4-c3'; +import { ParseErrorListener } from '../common/parseErrorListener'; +import { Parser, Token } from 'antlr4ng'; +import { MySqlParser } from '../../lib/mysql/MySqlParser'; + +export class MysqlErrorListener extends ParseErrorListener { + private preferredRules: Set; + + private objectNames: Map = new Map([ + [MySqlParser.RULE_databaseName, 'database'], + [MySqlParser.RULE_databaseNameCreate, 'database'], + [MySqlParser.RULE_tableName, 'table'], + [MySqlParser.RULE_tableNameCreate, 'table'], + [MySqlParser.RULE_viewName, 'view'], + [MySqlParser.RULE_viewNameCreate, 'view'], + [MySqlParser.RULE_functionName, 'function'], + [MySqlParser.RULE_functionNameCreate, 'function'], + [MySqlParser.RULE_columnName, 'column'], + [MySqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener, preferredRules) { + super(errorListener); + this.preferredRules = preferredRules; + } + + public getExpectedText(parser: Parser, token: Token) { + let expectedText = ''; + + let currentContext = parser.context; + 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) { + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case MySqlParser.RULE_databaseName: + case MySqlParser.RULE_tableName: + case MySqlParser.RULE_functionName: + case MySqlParser.RULE_viewName: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case MySqlParser.RULE_databaseNameCreate: + case MySqlParser.RULE_tableNameCreate: + case MySqlParser.RULE_functionNameCreate: + case MySqlParser.RULE_viewNameCreate: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} diff --git a/test/parser/mysql/errorListener.test.ts b/test/parser/mysql/errorListener.test.ts new file mode 100644 index 00000000..03ffef30 --- /dev/null +++ b/test/parser/mysql/errorListener.test.ts @@ -0,0 +1,48 @@ +import { MySQL } from 'src/parser/mysql'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SHOW CREATE DATABASE `; +const sql3 = `SHOW CREATE DATABASE IF NOT EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; + +describe('MySQL validate invalid sql and test msg', () => { + const mysql = new MySQL(); + + test('validate random text', () => { + const errors = mysql.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = mysql.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = mysql.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'statement is incomplete, expecting a new database name or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = mysql.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = mysql.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'froma' is not valid at this position, expecting a keyword` + ); + }); +}); From 56ab9d0d26a46e8d820cbd145837ac475a965557 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Thu, 28 Mar 2024 17:48:39 +0800 Subject: [PATCH 2/6] feat: improve other sql error msg --- src/parser/common/basicSQL.ts | 4 +- src/parser/flink/flinkErrorListener.ts | 78 +++++++++++++++++ src/parser/flink/index.ts | 6 ++ src/parser/hive/hiveErrorListener.ts | 79 +++++++++++++++++ src/parser/hive/index.ts | 6 ++ src/parser/impala/ImpalaErrorListener.ts | 77 +++++++++++++++++ src/parser/impala/index.ts | 6 ++ src/parser/mysql/index.ts | 2 +- src/parser/mysql/mysqlErrorListener.ts | 12 +-- src/parser/postgresql/index.ts | 6 ++ src/parser/postgresql/postgreErrorListener.ts | 85 +++++++++++++++++++ src/parser/spark/index.ts | 6 ++ src/parser/spark/sparkErrorListener.ts | 77 +++++++++++++++++ src/parser/trino/index.ts | 6 ++ src/parser/trino/trinoErrorListener.ts | 79 +++++++++++++++++ 15 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 src/parser/flink/flinkErrorListener.ts create mode 100644 src/parser/hive/hiveErrorListener.ts create mode 100644 src/parser/impala/ImpalaErrorListener.ts create mode 100644 src/parser/postgresql/postgreErrorListener.ts create mode 100644 src/parser/spark/sparkErrorListener.ts create mode 100644 src/parser/trino/trinoErrorListener.ts diff --git a/src/parser/common/basicSQL.ts b/src/parser/common/basicSQL.ts index 047c4886..8f28dcc7 100644 --- a/src/parser/common/basicSQL.ts +++ b/src/parser/common/basicSQL.ts @@ -15,7 +15,7 @@ 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 { ParseError, ErrorListener } from './parseErrorListener'; import { ErrorStrategy } from './errorStrategy'; import type { SplitListener } from './splitListener'; import type { EntityCollector } from './entityCollector'; @@ -82,7 +82,7 @@ export abstract class BasicSQL< /** * Get a new errorListener instance. */ - protected abstract createErrorListener(errorListener: ErrorListener): ANTLRErrorListener; + protected abstract createErrorListener(errorListener: ErrorListener): ANTLRErrorListener; /** * Get a new entityCollector instance. diff --git a/src/parser/flink/flinkErrorListener.ts b/src/parser/flink/flinkErrorListener.ts new file mode 100644 index 00000000..9ce36b32 --- /dev/null +++ b/src/parser/flink/flinkErrorListener.ts @@ -0,0 +1,78 @@ +import { CodeCompletionCore } from 'antlr4-c3'; +import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; +import { Parser, Token } from 'antlr4ng'; +import { FlinkSqlParser } from '../../lib/flink/FlinkSqlParser'; + +export class FlinkErrorListener extends ParseErrorListener { + private preferredRules: Set; + + private objectNames: Map = new Map([ + [FlinkSqlParser.RULE_catalogPath, '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) { + super(errorListener); + 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) { + // 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: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case FlinkSqlParser.RULE_databasePathCreate: + case FlinkSqlParser.RULE_tablePathCreate: + case FlinkSqlParser.RULE_functionNameCreate: + case FlinkSqlParser.RULE_viewPathCreate: + case FlinkSqlParser.RULE_columnNameCreate: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} diff --git a/src/parser/flink/index.ts b/src/parser/flink/index.ts index eb727986..d9d053fb 100644 --- a/src/parser/flink/index.ts +++ b/src/parser/flink/index.ts @@ -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 }; @@ -37,6 +39,10 @@ export class FlinkSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [HiveSqlParser.RULE_dbSchemaName, 'database'], + [HiveSqlParser.RULE_dbSchemaNameCreate, 'database'], + [HiveSqlParser.RULE_tableName, 'table'], + [HiveSqlParser.RULE_tableNameCreate, 'table'], + [HiveSqlParser.RULE_viewName, 'view'], + [HiveSqlParser.RULE_viewNameCreate, 'view'], + [HiveSqlParser.RULE_functionNameForDDL, 'function'], + [HiveSqlParser.RULE_functionNameForInvoke, 'function'], + [HiveSqlParser.RULE_functionNameCreate, 'function'], + [HiveSqlParser.RULE_columnName, 'column'], + [HiveSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set) { + super(errorListener); + 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) { + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case HiveSqlParser.RULE_dbSchemaName: + case HiveSqlParser.RULE_tableName: + case HiveSqlParser.RULE_viewName: + case HiveSqlParser.RULE_functionNameForDDL: + case HiveSqlParser.RULE_functionNameForInvoke: + case HiveSqlParser.RULE_columnName: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case HiveSqlParser.RULE_dbSchemaNameCreate: + case HiveSqlParser.RULE_tableNameCreate: + case HiveSqlParser.RULE_functionNameCreate: + case HiveSqlParser.RULE_viewNameCreate: + case HiveSqlParser.RULE_columnNameCreate: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} diff --git a/src/parser/hive/index.ts b/src/parser/hive/index.ts index c0c1c8ba..dd16d072 100644 --- a/src/parser/hive/index.ts +++ b/src/parser/hive/index.ts @@ -8,6 +8,8 @@ import { EntityContextType, Suggestions, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { HiveSqlSplitListener } from './hiveSplitListener'; import { HiveEntityCollector } from './hiveEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { HiveErrorListener } from './hiveErrorListener'; export { HiveEntityCollector, HiveSqlSplitListener }; @@ -38,6 +40,10 @@ export class HiveSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [ImpalaSqlParser.RULE_databaseNamePath, 'database'], + [ImpalaSqlParser.RULE_databaseNameCreate, 'database'], + [ImpalaSqlParser.RULE_tableNamePath, 'table'], + [ImpalaSqlParser.RULE_tableNameCreate, 'table'], + [ImpalaSqlParser.RULE_viewNamePath, 'view'], + [ImpalaSqlParser.RULE_viewNameCreate, 'view'], + [ImpalaSqlParser.RULE_functionNamePath, 'function'], + [ImpalaSqlParser.RULE_functionNameCreate, 'function'], + [ImpalaSqlParser.RULE_columnNamePath, 'column'], + [ImpalaSqlParser.RULE_columnNamePathCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set) { + super(errorListener); + 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) { + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case ImpalaSqlParser.RULE_databaseNamePath: + case ImpalaSqlParser.RULE_tableNamePath: + case ImpalaSqlParser.RULE_functionNamePath: + case ImpalaSqlParser.RULE_viewNamePath: + case ImpalaSqlParser.RULE_columnNamePath: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case ImpalaSqlParser.RULE_databaseNameCreate: + case ImpalaSqlParser.RULE_tableNameCreate: + case ImpalaSqlParser.RULE_functionNameCreate: + case ImpalaSqlParser.RULE_viewNameCreate: + case ImpalaSqlParser.RULE_columnNamePathCreate: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} diff --git a/src/parser/impala/index.ts b/src/parser/impala/index.ts index 5afb965a..01624341 100644 --- a/src/parser/impala/index.ts +++ b/src/parser/impala/index.ts @@ -7,6 +7,8 @@ import { EntityContextType, Suggestions, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { ImpalaSqlSplitListener } from './impalaSplitListener'; import { ImpalaEntityCollector } from './impalaEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { ImpalaErrorListener } from './ImpalaErrorListener'; export { ImpalaEntityCollector, ImpalaSqlSplitListener }; @@ -36,6 +38,10 @@ export class ImpalaSQL extends BasicSQL { return new MysqlSplitListener(); } - protected createErrorListener(_errorListener: ErrorListener) { + protected createErrorListener(_errorListener: ErrorListener) { return new MysqlErrorListener(_errorListener, this.preferredRules); } diff --git a/src/parser/mysql/mysqlErrorListener.ts b/src/parser/mysql/mysqlErrorListener.ts index 307eb510..4aeb487f 100644 --- a/src/parser/mysql/mysqlErrorListener.ts +++ b/src/parser/mysql/mysqlErrorListener.ts @@ -1,5 +1,5 @@ import { CodeCompletionCore } from 'antlr4-c3'; -import { ParseErrorListener } from '../common/parseErrorListener'; +import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; import { Parser, Token } from 'antlr4ng'; import { MySqlParser } from '../../lib/mysql/MySqlParser'; @@ -19,7 +19,7 @@ export class MysqlErrorListener extends ParseErrorListener { [MySqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener, preferredRules) { + constructor(errorListener: ErrorListener, preferredRules: Set) { super(errorListener); this.preferredRules = preferredRules; } @@ -27,7 +27,7 @@ export class MysqlErrorListener extends ParseErrorListener { public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; - let currentContext = parser.context; + let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } @@ -45,7 +45,8 @@ export class MysqlErrorListener extends ParseErrorListener { case MySqlParser.RULE_databaseName: case MySqlParser.RULE_tableName: case MySqlParser.RULE_functionName: - case MySqlParser.RULE_viewName: { + case MySqlParser.RULE_viewName: + case MySqlParser.RULE_columnName: { if (!name) { expectedText = 'a new object name'; } else { @@ -56,7 +57,8 @@ export class MysqlErrorListener extends ParseErrorListener { case MySqlParser.RULE_databaseNameCreate: case MySqlParser.RULE_tableNameCreate: case MySqlParser.RULE_functionNameCreate: - case MySqlParser.RULE_viewNameCreate: { + case MySqlParser.RULE_viewNameCreate: + case MySqlParser.RULE_columnNameCreate: { if (!name) { expectedText = 'an existing object'; } else { diff --git a/src/parser/postgresql/index.ts b/src/parser/postgresql/index.ts index 4da31c7b..ba9ad257 100644 --- a/src/parser/postgresql/index.ts +++ b/src/parser/postgresql/index.ts @@ -8,6 +8,8 @@ import { BasicSQL } from '../common/basicSQL'; import { StmtContextType } from '../common/entityCollector'; import { PostgreSqlEntityCollector } from './postgreEntityCollector'; import { PostgreSqlSplitListener } from './postgreSplitListener'; +import { ErrorListener } from '../common/parseErrorListener'; +import { PostgreSqlErrorListener } from './postgreErrorListener'; export { PostgreSqlEntityCollector, PostgreSqlSplitListener }; @@ -41,6 +43,10 @@ export class PostgreSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [PostgreSqlParser.RULE_database_name, 'database'], + [PostgreSqlParser.RULE_database_name_create, 'database'], + [PostgreSqlParser.RULE_table_name, 'table'], + [PostgreSqlParser.RULE_table_name_create, 'table'], + [PostgreSqlParser.RULE_view_name, 'view'], + [PostgreSqlParser.RULE_view_name_create, 'view'], + [PostgreSqlParser.RULE_function_name, 'function'], + [PostgreSqlParser.RULE_function_name_create, 'function'], + [PostgreSqlParser.RULE_column_name, 'column'], + [PostgreSqlParser.RULE_column_name_create, 'column'], + [PostgreSqlParser.RULE_schema_name_create, 'schema'], + [PostgreSqlParser.RULE_schema_name, 'schema'], + [PostgreSqlParser.RULE_procedure_name_create, 'procedure'], + [PostgreSqlParser.RULE_procedure_name, 'procedure'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set) { + super(errorListener); + 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) { + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case PostgreSqlParser.RULE_table_name: + case PostgreSqlParser.RULE_function_name: + case PostgreSqlParser.RULE_schema_name: + case PostgreSqlParser.RULE_view_name: + case PostgreSqlParser.RULE_database_name: + case PostgreSqlParser.RULE_procedure_name: + case PostgreSqlParser.RULE_column_name: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case PostgreSqlParser.RULE_table_name_create: + case PostgreSqlParser.RULE_function_name_create: + case PostgreSqlParser.RULE_schema_name_create: + case PostgreSqlParser.RULE_view_name_create: + case PostgreSqlParser.RULE_database_name_create: + case PostgreSqlParser.RULE_procedure_name_create: + case PostgreSqlParser.RULE_column_name_create: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} diff --git a/src/parser/spark/index.ts b/src/parser/spark/index.ts index afbe5585..c744f0aa 100644 --- a/src/parser/spark/index.ts +++ b/src/parser/spark/index.ts @@ -7,6 +7,8 @@ import { Suggestions, EntityContextType, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { SparkSqlSplitListener } from './sparkSplitListener'; import { SparkEntityCollector } from './sparkEntityCollector'; +import { SparkErrorListener } from './sparkErrorListener'; +import { ErrorListener } from '../common/parseErrorListener'; export { SparkSqlSplitListener, SparkEntityCollector }; @@ -36,6 +38,10 @@ export class SparkSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [SparkSqlParser.RULE_namespaceName, 'namespace'], + [SparkSqlParser.RULE_namespaceNameCreate, 'namespace'], + [SparkSqlParser.RULE_tableName, 'table'], + [SparkSqlParser.RULE_tableNameCreate, 'table'], + [SparkSqlParser.RULE_viewName, 'view'], + [SparkSqlParser.RULE_viewNameCreate, 'view'], + [SparkSqlParser.RULE_functionName, 'function'], + [SparkSqlParser.RULE_functionNameCreate, 'function'], + [SparkSqlParser.RULE_columnName, 'column'], + [SparkSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set) { + super(errorListener); + 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) { + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case SparkSqlParser.RULE_namespaceName: + case SparkSqlParser.RULE_tableName: + case SparkSqlParser.RULE_viewName: + case SparkSqlParser.RULE_functionName: + case SparkSqlParser.RULE_columnName: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case SparkSqlParser.RULE_namespaceNameCreate: + case SparkSqlParser.RULE_tableNameCreate: + case SparkSqlParser.RULE_functionNameCreate: + case SparkSqlParser.RULE_viewNameCreate: + case SparkSqlParser.RULE_columnNameCreate: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} diff --git a/src/parser/trino/index.ts b/src/parser/trino/index.ts index 61ff9409..34f8e755 100644 --- a/src/parser/trino/index.ts +++ b/src/parser/trino/index.ts @@ -7,6 +7,8 @@ import { Suggestions, EntityContextType, SyntaxSuggestion } from '../common/type import { StmtContextType } from '../common/entityCollector'; import { TrinoSqlSplitListener } from './trinoSplitListener'; import { TrinoEntityCollector } from './trinoEntityCollector'; +import { ErrorListener } from '../common/parseErrorListener'; +import { TrinoErrorListener } from './trinoErrorListener'; export { TrinoSqlSplitListener, TrinoEntityCollector }; @@ -23,6 +25,10 @@ export class TrinoSQL extends BasicSQL; + + private objectNames: Map = new Map([ + [TrinoSqlParser.RULE_catalogName, 'database'], + [TrinoSqlParser.RULE_catalogNameCreate, 'database'], + [TrinoSqlParser.RULE_tableName, 'table'], + [TrinoSqlParser.RULE_tableNameCreate, 'table'], + [TrinoSqlParser.RULE_viewName, 'view'], + [TrinoSqlParser.RULE_viewNameCreate, 'view'], + [TrinoSqlParser.RULE_schemaName, 'schema'], + [TrinoSqlParser.RULE_schemaNameCreate, 'schema'], + [TrinoSqlParser.RULE_functionName, 'function'], + [TrinoSqlParser.RULE_columnName, 'column'], + [TrinoSqlParser.RULE_columnNameCreate, 'column'], + ]); + + constructor(errorListener: ErrorListener, preferredRules: Set) { + super(errorListener); + 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) { + // get expectedText as collect rules first + for (const candidate of candidates.rules) { + const [ruleType] = candidate; + const name = this.objectNames.get(ruleType); + switch (ruleType) { + case TrinoSqlParser.RULE_catalogName: + case TrinoSqlParser.RULE_schemaName: + case TrinoSqlParser.RULE_tableName: + case TrinoSqlParser.RULE_viewName: + case TrinoSqlParser.RULE_functionName: + case TrinoSqlParser.RULE_columnName: { + if (!name) { + expectedText = 'a new object name'; + } else { + expectedText = `a new ${name} name`; + } + break; + } + case TrinoSqlParser.RULE_catalogNameCreate: + case TrinoSqlParser.RULE_tableNameCreate: + case TrinoSqlParser.RULE_schemaNameCreate: + case TrinoSqlParser.RULE_viewNameCreate: + case TrinoSqlParser.RULE_tableNameCreate: { + if (!name) { + expectedText = 'an existing object'; + } else { + expectedText = `an existing ${name}`; + } + break; + } + } + } + } + if (candidates.tokens.size) { + expectedText += expectedText ? ' or a keyword' : 'a keyword'; + } + return expectedText; + } +} From b159196a01ada6d9fb698fea6b4c4e65af3a9ce4 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Thu, 11 Apr 2024 11:27:22 +0800 Subject: [PATCH 3/6] feat: support i18n for error msg --- src/locale/locale.json | 38 +++++++++++++++++ src/parser/common/basicSQL.ts | 4 +- src/parser/common/parseErrorListener.ts | 41 +++++++++++++------ src/parser/common/transformToI18n.ts | 17 ++++++++ src/parser/common/types.ts | 2 + src/parser/flink/flinkErrorListener.ts | 15 +++---- src/parser/flink/index.ts | 2 +- src/parser/hive/hiveErrorListener.ts | 15 +++---- src/parser/hive/index.ts | 2 +- src/parser/impala/ImpalaErrorListener.ts | 15 +++---- src/parser/impala/index.ts | 2 +- src/parser/mysql/index.ts | 2 +- src/parser/mysql/mysqlErrorListener.ts | 15 +++---- src/parser/postgresql/index.ts | 2 +- src/parser/postgresql/postgreErrorListener.ts | 15 +++---- src/parser/spark/index.ts | 2 +- src/parser/spark/sparkErrorListener.ts | 15 +++---- src/parser/trino/index.ts | 2 +- src/parser/trino/trinoErrorListener.ts | 15 +++---- test/parser/mysql/errorListener.test.ts | 36 +++++++++++++++- tsconfig.json | 2 + 21 files changed, 188 insertions(+), 71 deletions(-) create mode 100644 src/locale/locale.json create mode 100644 src/parser/common/transformToI18n.ts diff --git a/src/locale/locale.json b/src/locale/locale.json new file mode 100644 index 00000000..0b0ca21e --- /dev/null +++ b/src/locale/locale.json @@ -0,0 +1,38 @@ +{ + "zh_CN": { + "stmtInComplete": "语句不完整", + "noValidPosition": "在此位置无效", + "expecting": ",期望", + "unfinishedMultilineComment": "未完成的多行注释", + "unfinishedDoubleQuoted": "未完成的双引号字符串文字", + "unfinishedSingleQuoted": "未完成的单引号字符串文字", + "unfinishedTickQuoted": "未完成的反引号引用字符串文字", + "noValidInput": "根本没有有效的输入", + "newObj": "一个新的对象", + "existingObj": "一个存在的对象", + "new": "一个新的", + "existing": "一个存在的", + "orKeyword": "或者一个关键字", + "keyword": "一个关键字", + "missing": "缺少", + "at": "在" + }, + "en_US": { + "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 " + } +} diff --git a/src/parser/common/basicSQL.ts b/src/parser/common/basicSQL.ts index 8f28dcc7..8db50605 100644 --- a/src/parser/common/basicSQL.ts +++ b/src/parser/common/basicSQL.ts @@ -14,7 +14,7 @@ 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 { CaretPosition, LOCALE_TYPE, Suggestions, SyntaxSuggestion } from './types'; import { ParseError, ErrorListener } from './parseErrorListener'; import { ErrorStrategy } from './errorStrategy'; import type { SplitListener } from './splitListener'; @@ -92,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 diff --git a/src/parser/common/parseErrorListener.ts b/src/parser/common/parseErrorListener.ts index f1f85a9f..c955bfe8 100644 --- a/src/parser/common/parseErrorListener.ts +++ b/src/parser/common/parseErrorListener.ts @@ -10,6 +10,8 @@ import { InputMismatchException, NoViableAltException, } from 'antlr4ng'; +import { LOCALE_TYPE } from './types'; +import { transformToI18n } from './transformToI18n'; /** * Converted from {@link SyntaxError}. @@ -46,8 +48,10 @@ export type ErrorListener = (parseError: ParseError, originalError: SyntaxError) 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; } @@ -88,20 +92,32 @@ export abstract class ParseErrorListener implements ANTLRErrorListener { // handle missing or unwanted tokens. message = msg; if (msg.includes('extraneous')) { - message = `'${wrongText}' is not valid at this position${ - expectedText.length ? `, expecting ${expectedText}` : '' + 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 = `statement is incomplete`; + message = `{stmtInComplete}`; } else { - message = `'${wrongText}' is not valid at this position`; + message = `'${wrongText}' {noValidPosition}`; } if (expectedText.length > 0) { - message += `, expecting ${expectedText}`; + message += `{expecting}${expectedText}`; } } else { message = msg; @@ -117,24 +133,25 @@ export abstract class ParseErrorListener implements ANTLRErrorListener { ); switch (text[0]) { case '/': - message = 'Unfinished multiline comment'; + message = '{unfinishedMultilineComment}'; break; case '"': - message = 'Unfinished double quoted string literal'; + message = '{unfinishedDoubleQuoted}'; break; case "'": - message = 'Unfinished single quoted string literal'; + message = '{unfinishedSingleQuoted}'; break; case '`': - message = 'Unfinished back tick quoted string literal'; + message = '{unfinishedTickQuoted}'; break; default: - message = '"' + text + '" is no valid input at all'; + message = '"' + text + '" {noValidInput}'; break; } } } + message = transformToI18n(message, this.locale); let endCol = charPositionInLine + 1; if (offendingSymbol && offendingSymbol.text !== null) { endCol = charPositionInLine + offendingSymbol.text.length; @@ -146,7 +163,7 @@ export abstract class ParseErrorListener implements ANTLRErrorListener { endLine: line, startColumn: charPositionInLine + 1, endColumn: endCol + 1, - message: message, + message, }, { e, diff --git a/src/parser/common/transformToI18n.ts b/src/parser/common/transformToI18n.ts new file mode 100644 index 00000000..1a722d05 --- /dev/null +++ b/src/parser/common/transformToI18n.ts @@ -0,0 +1,17 @@ +import { LOCALE_TYPE } from './types'; +import i18n from '../../locale/locale.json'; + +/** + * transform message to locale language + * @param message error msg + * @param locale language setting + */ +function transformToI18n(message: string, locale: LOCALE_TYPE) { + const regex = /{([^}]+)}/g; + return message.replace( + regex, + (_, key: keyof (typeof i18n)[typeof locale]) => i18n[locale][key] || '' + ); +} + +export { transformToI18n }; diff --git a/src/parser/common/types.ts b/src/parser/common/types.ts index 853673ce..ffb7bdb6 100644 --- a/src/parser/common/types.ts +++ b/src/parser/common/types.ts @@ -67,3 +67,5 @@ export interface Suggestions { */ readonly keywords: string[]; } + +export type LOCALE_TYPE = 'zh_CN' | 'en_US'; diff --git a/src/parser/flink/flinkErrorListener.ts b/src/parser/flink/flinkErrorListener.ts index 9ce36b32..7efa6071 100644 --- a/src/parser/flink/flinkErrorListener.ts +++ b/src/parser/flink/flinkErrorListener.ts @@ -2,6 +2,7 @@ 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; @@ -20,8 +21,8 @@ export class FlinkErrorListener extends ParseErrorListener { [FlinkSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -49,9 +50,9 @@ export class FlinkErrorListener extends ParseErrorListener { case FlinkSqlParser.RULE_functionName: case FlinkSqlParser.RULE_columnName: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -61,9 +62,9 @@ export class FlinkErrorListener extends ParseErrorListener { case FlinkSqlParser.RULE_viewPathCreate: case FlinkSqlParser.RULE_columnNameCreate: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -71,7 +72,7 @@ export class FlinkErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/src/parser/flink/index.ts b/src/parser/flink/index.ts index d9d053fb..61d19d22 100644 --- a/src/parser/flink/index.ts +++ b/src/parser/flink/index.ts @@ -40,7 +40,7 @@ export class FlinkSQL extends BasicSQL; @@ -20,8 +21,8 @@ export class HiveErrorListener extends ParseErrorListener { [HiveSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -50,9 +51,9 @@ export class HiveErrorListener extends ParseErrorListener { case HiveSqlParser.RULE_functionNameForInvoke: case HiveSqlParser.RULE_columnName: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -62,9 +63,9 @@ export class HiveErrorListener extends ParseErrorListener { case HiveSqlParser.RULE_viewNameCreate: case HiveSqlParser.RULE_columnNameCreate: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -72,7 +73,7 @@ export class HiveErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/src/parser/hive/index.ts b/src/parser/hive/index.ts index dd16d072..7bac2468 100644 --- a/src/parser/hive/index.ts +++ b/src/parser/hive/index.ts @@ -41,7 +41,7 @@ export class HiveSQL extends BasicSQL; @@ -19,8 +20,8 @@ export class ImpalaErrorListener extends ParseErrorListener { [ImpalaSqlParser.RULE_columnNamePathCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -48,9 +49,9 @@ export class ImpalaErrorListener extends ParseErrorListener { case ImpalaSqlParser.RULE_viewNamePath: case ImpalaSqlParser.RULE_columnNamePath: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -60,9 +61,9 @@ export class ImpalaErrorListener extends ParseErrorListener { case ImpalaSqlParser.RULE_viewNameCreate: case ImpalaSqlParser.RULE_columnNamePathCreate: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -70,7 +71,7 @@ export class ImpalaErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/src/parser/impala/index.ts b/src/parser/impala/index.ts index 01624341..9902507e 100644 --- a/src/parser/impala/index.ts +++ b/src/parser/impala/index.ts @@ -39,7 +39,7 @@ export class ImpalaSQL extends BasicSQL { } protected createErrorListener(_errorListener: ErrorListener) { - return new MysqlErrorListener(_errorListener, this.preferredRules); + return new MysqlErrorListener(_errorListener, this.preferredRules, this.locale); } protected createEntityCollector(input: string, caretTokenIndex?: number) { diff --git a/src/parser/mysql/mysqlErrorListener.ts b/src/parser/mysql/mysqlErrorListener.ts index 4aeb487f..b397990f 100644 --- a/src/parser/mysql/mysqlErrorListener.ts +++ b/src/parser/mysql/mysqlErrorListener.ts @@ -2,6 +2,7 @@ import { CodeCompletionCore } from 'antlr4-c3'; import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; import { Parser, Token } from 'antlr4ng'; import { MySqlParser } from '../../lib/mysql/MySqlParser'; +import { LOCALE_TYPE } from '../common/types'; export class MysqlErrorListener extends ParseErrorListener { private preferredRules: Set; @@ -19,8 +20,8 @@ export class MysqlErrorListener extends ParseErrorListener { [MySqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -48,9 +49,9 @@ export class MysqlErrorListener extends ParseErrorListener { case MySqlParser.RULE_viewName: case MySqlParser.RULE_columnName: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -60,9 +61,9 @@ export class MysqlErrorListener extends ParseErrorListener { case MySqlParser.RULE_viewNameCreate: case MySqlParser.RULE_columnNameCreate: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -70,7 +71,7 @@ export class MysqlErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/src/parser/postgresql/index.ts b/src/parser/postgresql/index.ts index ba9ad257..63820000 100644 --- a/src/parser/postgresql/index.ts +++ b/src/parser/postgresql/index.ts @@ -44,7 +44,7 @@ export class PostgreSQL extends BasicSQL; @@ -23,8 +24,8 @@ export class PostgreSqlErrorListener extends ParseErrorListener { [PostgreSqlParser.RULE_procedure_name, 'procedure'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -54,9 +55,9 @@ export class PostgreSqlErrorListener extends ParseErrorListener { case PostgreSqlParser.RULE_procedure_name: case PostgreSqlParser.RULE_column_name: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -68,9 +69,9 @@ export class PostgreSqlErrorListener extends ParseErrorListener { case PostgreSqlParser.RULE_procedure_name_create: case PostgreSqlParser.RULE_column_name_create: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -78,7 +79,7 @@ export class PostgreSqlErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/src/parser/spark/index.ts b/src/parser/spark/index.ts index c744f0aa..1b847fd8 100644 --- a/src/parser/spark/index.ts +++ b/src/parser/spark/index.ts @@ -39,7 +39,7 @@ export class SparkSQL extends BasicSQL; @@ -19,8 +20,8 @@ export class SparkErrorListener extends ParseErrorListener { [SparkSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -48,9 +49,9 @@ export class SparkErrorListener extends ParseErrorListener { case SparkSqlParser.RULE_functionName: case SparkSqlParser.RULE_columnName: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -60,9 +61,9 @@ export class SparkErrorListener extends ParseErrorListener { case SparkSqlParser.RULE_viewNameCreate: case SparkSqlParser.RULE_columnNameCreate: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -70,7 +71,7 @@ export class SparkErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/src/parser/trino/index.ts b/src/parser/trino/index.ts index 34f8e755..554b0b15 100644 --- a/src/parser/trino/index.ts +++ b/src/parser/trino/index.ts @@ -26,7 +26,7 @@ export class TrinoSQL extends BasicSQL; @@ -20,8 +21,8 @@ export class TrinoErrorListener extends ParseErrorListener { [TrinoSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set) { - super(errorListener); + constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { + super(errorListener, locale); this.preferredRules = preferredRules; } @@ -50,9 +51,9 @@ export class TrinoErrorListener extends ParseErrorListener { case TrinoSqlParser.RULE_functionName: case TrinoSqlParser.RULE_columnName: { if (!name) { - expectedText = 'a new object name'; + expectedText = '{newObj}'; } else { - expectedText = `a new ${name} name`; + expectedText = `{new}${name}`; } break; } @@ -62,9 +63,9 @@ export class TrinoErrorListener extends ParseErrorListener { case TrinoSqlParser.RULE_viewNameCreate: case TrinoSqlParser.RULE_tableNameCreate: { if (!name) { - expectedText = 'an existing object'; + expectedText = '{existingObj}'; } else { - expectedText = `an existing ${name}`; + expectedText = `{existing}${name}`; } break; } @@ -72,7 +73,7 @@ export class TrinoErrorListener extends ParseErrorListener { } } if (candidates.tokens.size) { - expectedText += expectedText ? ' or a keyword' : 'a keyword'; + expectedText += expectedText ? '{orKeyword}' : '{keyword}'; } return expectedText; } diff --git a/test/parser/mysql/errorListener.test.ts b/test/parser/mysql/errorListener.test.ts index 03ffef30..7843ee38 100644 --- a/test/parser/mysql/errorListener.test.ts +++ b/test/parser/mysql/errorListener.test.ts @@ -27,7 +27,7 @@ describe('MySQL validate invalid sql and test msg', () => { const errors = mysql.validate(sql2); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - 'statement is incomplete, expecting a new database name or a keyword' + 'statement is incomplete, expecting a new database or a keyword' ); }); @@ -42,7 +42,39 @@ describe('MySQL validate invalid sql and test msg', () => { const errors = mysql.validate(sql4); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `'froma' is not valid at this position, expecting a keyword` + `'froma' is not valid at this position, expecting a new column or a keyword` ); }); + + test('validate random text cn', () => { + mysql.locale = 'zh_CN'; + const errors = mysql.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = mysql.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = mysql.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整,期望一个新的database或者一个关键字'); + }); + + test('validate unComplete sql3 cn', () => { + const errors = mysql.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = mysql.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个新的column或者一个关键字`); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 2329d6f8..c27a683b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,8 @@ "noImplicitReturns": true, "noImplicitThis": true, "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, "lib": [ "ESNext", "DOM" From 56269914e151bbaae1cb66e9be9cc628b343e1d2 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Fri, 31 May 2024 16:50:00 +0800 Subject: [PATCH 4/6] feat: add all sql errorMsg unit test --- src/locale/locale.json | 6 +- src/parser/flink/flinkErrorListener.ts | 21 ++-- src/parser/hive/hiveErrorListener.ts | 14 +-- src/parser/impala/ImpalaErrorListener.ts | 14 +-- src/parser/mysql/mysqlErrorListener.ts | 14 +-- src/parser/postgresql/postgreErrorListener.ts | 14 +-- src/parser/spark/sparkErrorListener.ts | 14 +-- src/parser/trino/trinoErrorListener.ts | 18 ++-- test/parser/flink/errorListener.test.ts | 100 ++++++++++++++++++ test/parser/hive/errorListener.test.ts | 91 ++++++++++++++++ test/parser/impala/errorListener.test.ts | 91 ++++++++++++++++ test/parser/mysql/errorListener.test.ts | 8 +- test/parser/postgresql/errorListener.test.ts | 80 ++++++++++++++ test/parser/spark/errorListener.test.ts | 67 ++++++++++++ test/parser/trino/errorListener.test.ts | 63 +++++++++++ 15 files changed, 535 insertions(+), 80 deletions(-) create mode 100644 test/parser/flink/errorListener.test.ts create mode 100644 test/parser/hive/errorListener.test.ts create mode 100644 test/parser/impala/errorListener.test.ts create mode 100644 test/parser/postgresql/errorListener.test.ts create mode 100644 test/parser/spark/errorListener.test.ts create mode 100644 test/parser/trino/errorListener.test.ts diff --git a/src/locale/locale.json b/src/locale/locale.json index 0b0ca21e..b0f0faad 100644 --- a/src/locale/locale.json +++ b/src/locale/locale.json @@ -15,7 +15,8 @@ "orKeyword": "或者一个关键字", "keyword": "一个关键字", "missing": "缺少", - "at": "在" + "at": "在", + "or": "或者" }, "en_US": { "stmtInComplete": "statement is incomplete", @@ -33,6 +34,7 @@ "orKeyword": " or a keyword", "keyword": "a keyword", "missing": "missing ", - "at": " at " + "at": " at ", + "or": " or " } } diff --git a/src/parser/flink/flinkErrorListener.ts b/src/parser/flink/flinkErrorListener.ts index 7efa6071..61a8a157 100644 --- a/src/parser/flink/flinkErrorListener.ts +++ b/src/parser/flink/flinkErrorListener.ts @@ -9,6 +9,7 @@ export class FlinkErrorListener extends ParseErrorListener { private objectNames: Map = new Map([ [FlinkSqlParser.RULE_catalogPath, 'catalog'], + [FlinkSqlParser.RULE_catalogPathCreate, 'catalog'], [FlinkSqlParser.RULE_databasePath, 'database'], [FlinkSqlParser.RULE_databasePathCreate, 'database'], [FlinkSqlParser.RULE_tablePath, 'table'], @@ -39,6 +40,7 @@ export class FlinkErrorListener extends ParseErrorListener { 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; @@ -48,28 +50,23 @@ export class FlinkErrorListener extends ParseErrorListener { case FlinkSqlParser.RULE_tablePath: case FlinkSqlParser.RULE_viewPath: case FlinkSqlParser.RULE_functionName: - case FlinkSqlParser.RULE_columnName: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + 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: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + 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}'; diff --git a/src/parser/hive/hiveErrorListener.ts b/src/parser/hive/hiveErrorListener.ts index d4238d4b..6435806e 100644 --- a/src/parser/hive/hiveErrorListener.ts +++ b/src/parser/hive/hiveErrorListener.ts @@ -39,6 +39,7 @@ export class HiveErrorListener extends ParseErrorListener { 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; @@ -50,11 +51,7 @@ export class HiveErrorListener extends ParseErrorListener { case HiveSqlParser.RULE_functionNameForDDL: case HiveSqlParser.RULE_functionNameForInvoke: case HiveSqlParser.RULE_columnName: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + result.push(`{existing}${name}`); break; } case HiveSqlParser.RULE_dbSchemaNameCreate: @@ -62,15 +59,12 @@ export class HiveErrorListener extends ParseErrorListener { case HiveSqlParser.RULE_functionNameCreate: case HiveSqlParser.RULE_viewNameCreate: case HiveSqlParser.RULE_columnNameCreate: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + result.push(`{new}${name}`); break; } } } + expectedText = result.join('{or}'); } if (candidates.tokens.size) { expectedText += expectedText ? '{orKeyword}' : '{keyword}'; diff --git a/src/parser/impala/ImpalaErrorListener.ts b/src/parser/impala/ImpalaErrorListener.ts index c9b5f7a6..c09dc724 100644 --- a/src/parser/impala/ImpalaErrorListener.ts +++ b/src/parser/impala/ImpalaErrorListener.ts @@ -38,6 +38,7 @@ export class ImpalaErrorListener extends ParseErrorListener { 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; @@ -48,11 +49,7 @@ export class ImpalaErrorListener extends ParseErrorListener { case ImpalaSqlParser.RULE_functionNamePath: case ImpalaSqlParser.RULE_viewNamePath: case ImpalaSqlParser.RULE_columnNamePath: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + result.push(`{existing}${name}`); break; } case ImpalaSqlParser.RULE_databaseNameCreate: @@ -60,15 +57,12 @@ export class ImpalaErrorListener extends ParseErrorListener { case ImpalaSqlParser.RULE_functionNameCreate: case ImpalaSqlParser.RULE_viewNameCreate: case ImpalaSqlParser.RULE_columnNamePathCreate: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + result.push(`{new}${name}`); break; } } } + expectedText = result.join(`{or}`); } if (candidates.tokens.size) { expectedText += expectedText ? '{orKeyword}' : '{keyword}'; diff --git a/src/parser/mysql/mysqlErrorListener.ts b/src/parser/mysql/mysqlErrorListener.ts index b397990f..ba6c1440 100644 --- a/src/parser/mysql/mysqlErrorListener.ts +++ b/src/parser/mysql/mysqlErrorListener.ts @@ -38,6 +38,7 @@ export class MysqlErrorListener extends ParseErrorListener { 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; @@ -48,11 +49,7 @@ export class MysqlErrorListener extends ParseErrorListener { case MySqlParser.RULE_functionName: case MySqlParser.RULE_viewName: case MySqlParser.RULE_columnName: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + result.push(`{existing}${name}`); break; } case MySqlParser.RULE_databaseNameCreate: @@ -60,15 +57,12 @@ export class MysqlErrorListener extends ParseErrorListener { case MySqlParser.RULE_functionNameCreate: case MySqlParser.RULE_viewNameCreate: case MySqlParser.RULE_columnNameCreate: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + result.push(`{new}${name}`); break; } } } + expectedText = result.join(`{or}`); } if (candidates.tokens.size) { expectedText += expectedText ? '{orKeyword}' : '{keyword}'; diff --git a/src/parser/postgresql/postgreErrorListener.ts b/src/parser/postgresql/postgreErrorListener.ts index 2dfb650b..19b99848 100644 --- a/src/parser/postgresql/postgreErrorListener.ts +++ b/src/parser/postgresql/postgreErrorListener.ts @@ -42,6 +42,7 @@ export class PostgreSqlErrorListener extends ParseErrorListener { 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; @@ -54,11 +55,7 @@ export class PostgreSqlErrorListener extends ParseErrorListener { case PostgreSqlParser.RULE_database_name: case PostgreSqlParser.RULE_procedure_name: case PostgreSqlParser.RULE_column_name: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + result.push(`{existing}${name}`); break; } case PostgreSqlParser.RULE_table_name_create: @@ -68,15 +65,12 @@ export class PostgreSqlErrorListener extends ParseErrorListener { case PostgreSqlParser.RULE_database_name_create: case PostgreSqlParser.RULE_procedure_name_create: case PostgreSqlParser.RULE_column_name_create: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + result.push(`{new}${name}`); break; } } } + expectedText = result.join('{or}'); } if (candidates.tokens.size) { expectedText += expectedText ? '{orKeyword}' : '{keyword}'; diff --git a/src/parser/spark/sparkErrorListener.ts b/src/parser/spark/sparkErrorListener.ts index 2c505eae..b2035655 100644 --- a/src/parser/spark/sparkErrorListener.ts +++ b/src/parser/spark/sparkErrorListener.ts @@ -38,6 +38,7 @@ export class SparkErrorListener extends ParseErrorListener { const candidates = core.collectCandidates(token.tokenIndex, currentContext); if (candidates.rules.size) { + const result = []; // get expectedText as collect rules first for (const candidate of candidates.rules) { const [ruleType] = candidate; @@ -48,11 +49,7 @@ export class SparkErrorListener extends ParseErrorListener { case SparkSqlParser.RULE_viewName: case SparkSqlParser.RULE_functionName: case SparkSqlParser.RULE_columnName: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + result.push(`{existing}${name}`); break; } case SparkSqlParser.RULE_namespaceNameCreate: @@ -60,15 +57,12 @@ export class SparkErrorListener extends ParseErrorListener { case SparkSqlParser.RULE_functionNameCreate: case SparkSqlParser.RULE_viewNameCreate: case SparkSqlParser.RULE_columnNameCreate: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + result.push(`{new}${name}`); break; } } } + expectedText = result.join('{or}'); } if (candidates.tokens.size) { expectedText += expectedText ? '{orKeyword}' : '{keyword}'; diff --git a/src/parser/trino/trinoErrorListener.ts b/src/parser/trino/trinoErrorListener.ts index 9890740c..01722234 100644 --- a/src/parser/trino/trinoErrorListener.ts +++ b/src/parser/trino/trinoErrorListener.ts @@ -8,8 +8,8 @@ export class TrinoErrorListener extends ParseErrorListener { private preferredRules: Set; private objectNames: Map = new Map([ - [TrinoSqlParser.RULE_catalogName, 'database'], - [TrinoSqlParser.RULE_catalogNameCreate, 'database'], + [TrinoSqlParser.RULE_catalogName, 'catalog'], + [TrinoSqlParser.RULE_catalogNameCreate, 'catalog'], [TrinoSqlParser.RULE_tableName, 'table'], [TrinoSqlParser.RULE_tableNameCreate, 'table'], [TrinoSqlParser.RULE_viewName, 'view'], @@ -39,6 +39,7 @@ export class TrinoErrorListener extends ParseErrorListener { 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; @@ -50,11 +51,7 @@ export class TrinoErrorListener extends ParseErrorListener { case TrinoSqlParser.RULE_viewName: case TrinoSqlParser.RULE_functionName: case TrinoSqlParser.RULE_columnName: { - if (!name) { - expectedText = '{newObj}'; - } else { - expectedText = `{new}${name}`; - } + result.push(`{existing}${name}`); break; } case TrinoSqlParser.RULE_catalogNameCreate: @@ -62,15 +59,12 @@ export class TrinoErrorListener extends ParseErrorListener { case TrinoSqlParser.RULE_schemaNameCreate: case TrinoSqlParser.RULE_viewNameCreate: case TrinoSqlParser.RULE_tableNameCreate: { - if (!name) { - expectedText = '{existingObj}'; - } else { - expectedText = `{existing}${name}`; - } + result.push(`{new}${name}`); break; } } } + expectedText = result.join('{or}'); } if (candidates.tokens.size) { expectedText += expectedText ? '{orKeyword}' : '{keyword}'; diff --git a/test/parser/flink/errorListener.test.ts b/test/parser/flink/errorListener.test.ts new file mode 100644 index 00000000..3dfef5ef --- /dev/null +++ b/test/parser/flink/errorListener.test.ts @@ -0,0 +1,100 @@ +import { FlinkSQL } from 'src/parser/flink'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP VIEW IF EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; +const sql5 = `CREATE VIEW `; +const sql6 = `DROP CATALOG `; + +describe('FlinkSQL validate invalid sql and test msg', () => { + const flink = new FlinkSQL(); + + test('validate random text', () => { + const errors = flink.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = flink.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = flink.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'statement is incomplete, expecting an existing table or an existing view or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = flink.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = flink.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'aaa' is not valid at this position, expecting an existing column or a keyword` + ); + }); + + test('validate unComplete sql5', () => { + const errors = flink.validate(sql5); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `statement is incomplete, expecting a new view or a keyword` + ); + }); + + test('validate unComplete sql6', () => { + const errors = flink.validate(sql6); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `statement is incomplete, expecting an existing catalog or a keyword` + ); + }); + + test('validate random text cn', () => { + flink.locale = 'zh_CN'; + const errors = flink.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = flink.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = flink.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = flink.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = flink.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'aaa' 在此位置无效,期望一个存在的column或者一个关键字`); + }); +}); diff --git a/test/parser/hive/errorListener.test.ts b/test/parser/hive/errorListener.test.ts new file mode 100644 index 00000000..3b8cb2e2 --- /dev/null +++ b/test/parser/hive/errorListener.test.ts @@ -0,0 +1,91 @@ +import { HiveSQL } from 'src/parser/hive'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP VIEW IF EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; +const sql5 = `CREATE TABLE `; + +describe('HiveSQL validate invalid sql and test msg', () => { + const hive = new HiveSQL(); + + test('validate random text', () => { + const errors = hive.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = hive.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = hive.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'statement is incomplete, expecting an existing table or an existing view or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = hive.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = hive.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'froma' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql5', () => { + const errors = hive.validate(sql5); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `statement is incomplete, expecting a new table or a keyword` + ); + }); + + test('validate random text cn', () => { + hive.locale = 'zh_CN'; + const errors = hive.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = hive.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = hive.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = hive.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = hive.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个关键字`); + }); +}); diff --git a/test/parser/impala/errorListener.test.ts b/test/parser/impala/errorListener.test.ts new file mode 100644 index 00000000..af8403ab --- /dev/null +++ b/test/parser/impala/errorListener.test.ts @@ -0,0 +1,91 @@ +import { ImpalaSQL } from 'src/parser/impala'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP VIEW IF EXIsST aaa aaa`; +const sql4 = `SELECT * froma aaa`; +const sql5 = `CREATE VIEW `; + +describe('ImpalaSQL validate invalid sql and test msg', () => { + const impala = new ImpalaSQL(); + + test('validate random text', () => { + const errors = impala.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = impala.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = impala.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'statement is incomplete, expecting an existing table or an existing view or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = impala.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`missing 'EXISTS' at 'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' is not valid at this position`); + }); + + test('validate unComplete sql4', () => { + const errors = impala.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'froma' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql5', () => { + const errors = impala.validate(sql5); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `statement is incomplete, expecting a new view or a keyword` + ); + }); + + test('validate random text cn', () => { + impala.locale = 'zh_CN'; + const errors = impala.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = impala.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = impala.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = impala.validate(sql3); + expect(errors.length).toBe(2); + expect(errors[0].message).toBe(`缺少'EXISTS'在'EXIsST'`); + expect(errors[1].message).toBe(`'aaa' 在此位置无效`); + }); + + test('validate unComplete sql4 cn', () => { + const errors = impala.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个关键字`); + }); +}); diff --git a/test/parser/mysql/errorListener.test.ts b/test/parser/mysql/errorListener.test.ts index 7843ee38..9924a946 100644 --- a/test/parser/mysql/errorListener.test.ts +++ b/test/parser/mysql/errorListener.test.ts @@ -2,7 +2,7 @@ import { MySQL } from 'src/parser/mysql'; const randomText = `dhsdansdnkla ndjnsla ndnalks`; const sql1 = `SHOW CREATE TABLE`; -const sql2 = `SHOW CREATE DATABASE `; +const sql2 = `CREATE DATABASE `; const sql3 = `SHOW CREATE DATABASE IF NOT EXIsST aaa aaa`; const sql4 = `SELECT * froma aaa`; @@ -42,7 +42,7 @@ describe('MySQL validate invalid sql and test msg', () => { const errors = mysql.validate(sql4); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `'froma' is not valid at this position, expecting a new column or a keyword` + `'froma' is not valid at this position, expecting an existing column or a keyword` ); }); @@ -62,7 +62,7 @@ describe('MySQL validate invalid sql and test msg', () => { test('validate unComplete sql2 cn', () => { const errors = mysql.validate(sql2); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('语句不完整,期望一个新的database或者一个关键字'); + expect(errors[0].message).toEqual('语句不完整,期望一个新的database或者一个关键字'); }); test('validate unComplete sql3 cn', () => { @@ -75,6 +75,6 @@ describe('MySQL validate invalid sql and test msg', () => { test('validate unComplete sql4 cn', () => { const errors = mysql.validate(sql4); expect(errors.length).toBe(1); - expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个新的column或者一个关键字`); + expect(errors[0].message).toBe(`'froma' 在此位置无效,期望一个存在的column或者一个关键字`); }); }); diff --git a/test/parser/postgresql/errorListener.test.ts b/test/parser/postgresql/errorListener.test.ts new file mode 100644 index 00000000..6d353549 --- /dev/null +++ b/test/parser/postgresql/errorListener.test.ts @@ -0,0 +1,80 @@ +import { PostgreSQL } from 'src/parser/postgresql'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `ALTER EVENT`; +const sql2 = `CREATE FUNCTION `; +const sql3 = `SELECT name, altitude FROM ONLY cities WHERE `; +const sql4 = `DROP PROCEDURE name1 a`; + +describe('PostgreSQL validate invalid sql and test msg', () => { + const pgSQL = new PostgreSQL(); + + test('validate random text', () => { + const errors = pgSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = pgSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = pgSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete, expecting a new function'); + }); + + test('validate unComplete sql3', () => { + const errors = pgSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `statement is incomplete, expecting an existing function or an existing column or a keyword` + ); + }); + + test('validate unComplete sql4', () => { + const errors = pgSQL.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'a' is not valid at this position, expecting an existing procedure or a keyword` + ); + }); + + test('validate random text cn', () => { + pgSQL.locale = 'zh_CN'; + const errors = pgSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = pgSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = pgSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toEqual('语句不完整,期望一个新的function'); + }); + + test('validate unComplete sql3 cn', () => { + const errors = pgSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `语句不完整,期望一个存在的function或者一个存在的column或者一个关键字` + ); + }); + + test('validate unComplete sql4 cn', () => { + const errors = pgSQL.validate(sql4); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'a' 在此位置无效,期望一个存在的procedure或者一个关键字`); + }); +}); diff --git a/test/parser/spark/errorListener.test.ts b/test/parser/spark/errorListener.test.ts new file mode 100644 index 00000000..07830339 --- /dev/null +++ b/test/parser/spark/errorListener.test.ts @@ -0,0 +1,67 @@ +import { SparkSQL } from 'src/parser/spark'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `ALTER VIEW`; +const sql2 = `SELECT * FROM `; +const sql3 = `DROP SCHEMA aaa aaa`; + +describe('SparkSQL validate invalid sql and test msg', () => { + const spark = new SparkSQL(); + + test('validate random text', () => { + const errors = spark.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = spark.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = spark.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + 'statement is incomplete, expecting an existing table or an existing view or an existing function or a keyword' + ); + }); + + test('validate unComplete sql3', () => { + const errors = spark.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'aaa' is not valid at this position, expecting an existing namespace or a keyword` + ); + }); + + test('validate random text cn', () => { + spark.locale = 'zh_CN'; + const errors = spark.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = spark.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = spark.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + '语句不完整,期望一个存在的table或者一个存在的view或者一个存在的function或者一个关键字' + ); + }); + + test('validate unComplete sql3 cn', () => { + const errors = spark.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'aaa' 在此位置无效,期望一个存在的namespace或者一个关键字`); + }); +}); diff --git a/test/parser/trino/errorListener.test.ts b/test/parser/trino/errorListener.test.ts new file mode 100644 index 00000000..3e563f65 --- /dev/null +++ b/test/parser/trino/errorListener.test.ts @@ -0,0 +1,63 @@ +import { TrinoSQL } from 'src/parser/trino'; + +const randomText = `dhsdansdnkla ndjnsla ndnalks`; +const sql1 = `SHOW CREATE TABLE`; +const sql2 = `CREATE VIEW `; +const sql3 = `SHOW CREATE TABLE aaa aaa`; + +describe('TrinoSQL validate invalid sql and test msg', () => { + const trinoSQL = new TrinoSQL(); + + test('validate random text', () => { + const errors = trinoSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'dhsdansdnkla' is not valid at this position, expecting a keyword` + ); + }); + + test('validate unComplete sql1', () => { + const errors = trinoSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete'); + }); + + test('validate unComplete sql2', () => { + const errors = trinoSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('statement is incomplete, expecting a new view'); + }); + + test('validate unComplete sql3', () => { + const errors = trinoSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe( + `'aaa' is not valid at this position, expecting an existing table or a keyword` + ); + }); + + test('validate random text cn', () => { + trinoSQL.locale = 'zh_CN'; + const errors = trinoSQL.validate(randomText); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'dhsdansdnkla' 在此位置无效,期望一个关键字`); + }); + + test('validate unComplete sql1 cn', () => { + const errors = trinoSQL.validate(sql1); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('语句不完整'); + }); + + test('validate unComplete sql2 cn', () => { + const errors = trinoSQL.validate(sql2); + expect(errors.length).toBe(1); + expect(errors[0].message).toEqual('语句不完整,期望一个新的view'); + }); + + test('validate unComplete sql3 cn', () => { + const errors = trinoSQL.validate(sql3); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe(`'aaa' 在此位置无效,期望一个存在的table或者一个关键字`); + }); +}); From cecdbb8521b6299ca26d7db2211c5219fb9c962f Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Wed, 5 Jun 2024 10:25:14 +0800 Subject: [PATCH 5/6] feat: update locale file and change i18n funtion name --- src/locale/locale.json | 40 ---------------- src/locale/locale.ts | 46 +++++++++++++++++++ src/parser/common/parseErrorListener.ts | 4 +- .../{transformToI18n.ts => transform.ts} | 6 +-- 4 files changed, 51 insertions(+), 45 deletions(-) delete mode 100644 src/locale/locale.json create mode 100644 src/locale/locale.ts rename src/parser/common/{transformToI18n.ts => transform.ts} (69%) diff --git a/src/locale/locale.json b/src/locale/locale.json deleted file mode 100644 index b0f0faad..00000000 --- a/src/locale/locale.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "zh_CN": { - "stmtInComplete": "语句不完整", - "noValidPosition": "在此位置无效", - "expecting": ",期望", - "unfinishedMultilineComment": "未完成的多行注释", - "unfinishedDoubleQuoted": "未完成的双引号字符串文字", - "unfinishedSingleQuoted": "未完成的单引号字符串文字", - "unfinishedTickQuoted": "未完成的反引号引用字符串文字", - "noValidInput": "根本没有有效的输入", - "newObj": "一个新的对象", - "existingObj": "一个存在的对象", - "new": "一个新的", - "existing": "一个存在的", - "orKeyword": "或者一个关键字", - "keyword": "一个关键字", - "missing": "缺少", - "at": "在", - "or": "或者" - }, - "en_US": { - "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 " - } -} diff --git a/src/locale/locale.ts b/src/locale/locale.ts new file mode 100644 index 00000000..4f993acf --- /dev/null +++ b/src/locale/locale.ts @@ -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 }; diff --git a/src/parser/common/parseErrorListener.ts b/src/parser/common/parseErrorListener.ts index c955bfe8..902a36f3 100644 --- a/src/parser/common/parseErrorListener.ts +++ b/src/parser/common/parseErrorListener.ts @@ -11,7 +11,7 @@ import { NoViableAltException, } from 'antlr4ng'; import { LOCALE_TYPE } from './types'; -import { transformToI18n } from './transformToI18n'; +import { transform } from './transform'; /** * Converted from {@link SyntaxError}. @@ -151,7 +151,7 @@ export abstract class ParseErrorListener implements ANTLRErrorListener { } } } - message = transformToI18n(message, this.locale); + message = transform(message, this.locale); let endCol = charPositionInLine + 1; if (offendingSymbol && offendingSymbol.text !== null) { endCol = charPositionInLine + offendingSymbol.text.length; diff --git a/src/parser/common/transformToI18n.ts b/src/parser/common/transform.ts similarity index 69% rename from src/parser/common/transformToI18n.ts rename to src/parser/common/transform.ts index 1a722d05..f4e52317 100644 --- a/src/parser/common/transformToI18n.ts +++ b/src/parser/common/transform.ts @@ -1,12 +1,12 @@ import { LOCALE_TYPE } from './types'; -import i18n from '../../locale/locale.json'; +import { i18n } from '../../locale/locale'; /** * transform message to locale language * @param message error msg * @param locale language setting */ -function transformToI18n(message: string, locale: LOCALE_TYPE) { +function transform(message: string, locale: LOCALE_TYPE) { const regex = /{([^}]+)}/g; return message.replace( regex, @@ -14,4 +14,4 @@ function transformToI18n(message: string, locale: LOCALE_TYPE) { ); } -export { transformToI18n }; +export { transform }; From f467c4108e30a6a308c32d853dee8e0e0c47ba89 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Wed, 5 Jun 2024 11:10:03 +0800 Subject: [PATCH 6/6] test: upate error unit test --- test/parser/flink/errorListener.test.ts | 8 ++++---- test/parser/hive/errorListener.test.ts | 6 +++--- test/parser/impala/errorListener.test.ts | 6 +++--- test/parser/mysql/errorListener.test.ts | 4 ++-- test/parser/postgresql/errorListener.test.ts | 6 +++--- test/parser/spark/errorListener.test.ts | 4 ++-- test/parser/trino/errorListener.test.ts | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/test/parser/flink/errorListener.test.ts b/test/parser/flink/errorListener.test.ts index 3dfef5ef..1a82a9b3 100644 --- a/test/parser/flink/errorListener.test.ts +++ b/test/parser/flink/errorListener.test.ts @@ -22,14 +22,14 @@ describe('FlinkSQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = flink.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = flink.validate(sql2); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - 'statement is incomplete, expecting an existing table or an existing view or a keyword' + 'Statement is incomplete, expecting an existing table or an existing view or a keyword' ); }); @@ -52,7 +52,7 @@ describe('FlinkSQL validate invalid sql and test msg', () => { const errors = flink.validate(sql5); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `statement is incomplete, expecting a new view or a keyword` + `Statement is incomplete, expecting a new view or a keyword` ); }); @@ -60,7 +60,7 @@ describe('FlinkSQL validate invalid sql and test msg', () => { const errors = flink.validate(sql6); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `statement is incomplete, expecting an existing catalog or a keyword` + `Statement is incomplete, expecting an existing catalog or a keyword` ); }); diff --git a/test/parser/hive/errorListener.test.ts b/test/parser/hive/errorListener.test.ts index 3b8cb2e2..1bc9b6a1 100644 --- a/test/parser/hive/errorListener.test.ts +++ b/test/parser/hive/errorListener.test.ts @@ -21,14 +21,14 @@ describe('HiveSQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = hive.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = hive.validate(sql2); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - 'statement is incomplete, expecting an existing table or an existing view or a keyword' + 'Statement is incomplete, expecting an existing table or an existing view or a keyword' ); }); @@ -51,7 +51,7 @@ describe('HiveSQL validate invalid sql and test msg', () => { const errors = hive.validate(sql5); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `statement is incomplete, expecting a new table or a keyword` + `Statement is incomplete, expecting a new table or a keyword` ); }); diff --git a/test/parser/impala/errorListener.test.ts b/test/parser/impala/errorListener.test.ts index af8403ab..36a91b40 100644 --- a/test/parser/impala/errorListener.test.ts +++ b/test/parser/impala/errorListener.test.ts @@ -21,14 +21,14 @@ describe('ImpalaSQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = impala.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = impala.validate(sql2); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - 'statement is incomplete, expecting an existing table or an existing view or a keyword' + 'Statement is incomplete, expecting an existing table or an existing view or a keyword' ); }); @@ -51,7 +51,7 @@ describe('ImpalaSQL validate invalid sql and test msg', () => { const errors = impala.validate(sql5); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `statement is incomplete, expecting a new view or a keyword` + `Statement is incomplete, expecting a new view or a keyword` ); }); diff --git a/test/parser/mysql/errorListener.test.ts b/test/parser/mysql/errorListener.test.ts index 9924a946..443666bf 100644 --- a/test/parser/mysql/errorListener.test.ts +++ b/test/parser/mysql/errorListener.test.ts @@ -20,14 +20,14 @@ describe('MySQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = mysql.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = mysql.validate(sql2); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - 'statement is incomplete, expecting a new database or a keyword' + 'Statement is incomplete, expecting a new database or a keyword' ); }); diff --git a/test/parser/postgresql/errorListener.test.ts b/test/parser/postgresql/errorListener.test.ts index 6d353549..153f31ab 100644 --- a/test/parser/postgresql/errorListener.test.ts +++ b/test/parser/postgresql/errorListener.test.ts @@ -20,20 +20,20 @@ describe('PostgreSQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = pgSQL.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = pgSQL.validate(sql2); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete, expecting a new function'); + expect(errors[0].message).toBe('Statement is incomplete, expecting a new function'); }); test('validate unComplete sql3', () => { const errors = pgSQL.validate(sql3); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - `statement is incomplete, expecting an existing function or an existing column or a keyword` + `Statement is incomplete, expecting an existing function or an existing column or a keyword` ); }); diff --git a/test/parser/spark/errorListener.test.ts b/test/parser/spark/errorListener.test.ts index 07830339..bc356160 100644 --- a/test/parser/spark/errorListener.test.ts +++ b/test/parser/spark/errorListener.test.ts @@ -19,14 +19,14 @@ describe('SparkSQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = spark.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = spark.validate(sql2); expect(errors.length).toBe(1); expect(errors[0].message).toBe( - 'statement is incomplete, expecting an existing table or an existing view or an existing function or a keyword' + 'Statement is incomplete, expecting an existing table or an existing view or an existing function or a keyword' ); }); diff --git a/test/parser/trino/errorListener.test.ts b/test/parser/trino/errorListener.test.ts index 3e563f65..c4d174d6 100644 --- a/test/parser/trino/errorListener.test.ts +++ b/test/parser/trino/errorListener.test.ts @@ -19,13 +19,13 @@ describe('TrinoSQL validate invalid sql and test msg', () => { test('validate unComplete sql1', () => { const errors = trinoSQL.validate(sql1); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete'); + expect(errors[0].message).toBe('Statement is incomplete'); }); test('validate unComplete sql2', () => { const errors = trinoSQL.validate(sql2); expect(errors.length).toBe(1); - expect(errors[0].message).toBe('statement is incomplete, expecting a new view'); + expect(errors[0].message).toBe('Statement is incomplete, expecting a new view'); }); test('validate unComplete sql3', () => {