diff --git a/scripts/benchmark.js b/scripts/benchmark.js index 0ab348d9..01f104d6 100644 --- a/scripts/benchmark.js +++ b/scripts/benchmark.js @@ -43,7 +43,9 @@ function checkVersion() { if (semver.lt(currentVersion, MIN_VERSION)) { console.error( chalk.bold.red( - `Current Node.js version (v${currentVersion}) is lower than required version (v${semver.major(MIN_VERSION)}.x)` + `Current Node.js version (v${currentVersion}) is lower than required version (v${semver.major( + MIN_VERSION + )}.x)` ) ); return false; @@ -51,7 +53,9 @@ function checkVersion() { if (isRelease && semver.lt(currentVersion, RELEASE_VERSION)) { console.error( chalk.bold.red( - `Node.js version v${semver.major(RELEASE_VERSION)}.x+ is required for release benchmark!` + `Node.js version v${semver.major( + RELEASE_VERSION + )}.x+ is required for release benchmark!` ) ); return false; @@ -81,7 +85,9 @@ function prompt() { 'Cold start' + (isNodeVersionOk ? '' - : ` (Only supported on Node.js v${semver.major(RECOMMENDED_VERSION)}.x+)`), + : ` (Only supported on Node.js v${semver.major( + RECOMMENDED_VERSION + )}.x+)`), value: 'cold', disabled: !isNodeVersionOk, }, diff --git a/src/parser/common/basicSQL.ts b/src/parser/common/basicSQL.ts index b835979a..137adaa0 100644 --- a/src/parser/common/basicSQL.ts +++ b/src/parser/common/basicSQL.ts @@ -203,6 +203,13 @@ export abstract class BasicSQL< return this._parseErrors; } + /** + * Get the input string that has been parsed. + */ + public getParsedInput(): string { + return this._parsedInput; + } + /** * Get all Tokens of input string,'' is not included. * @param input source string @@ -252,35 +259,35 @@ export abstract class BasicSQL< } /** - * Get suggestions of syntax and token at caretPosition - * @param input source string - * @param caretPosition caret position, such as cursor position - * @returns suggestion + * Get a minimum boundary parser near tokenIndex. + * @param input source string. + * @param tokenIndex start from which index to minimize the boundary. + * @param originParseTree the parse tree need to be minimized, default value is the result of parsing `input`. + * @returns minimum parser info */ - public getSuggestionAtCaretPosition( + public getMinimumParserInfo( input: string, - caretPosition: CaretPosition - ): Suggestions | null { - const splitListener = this.splitListener; - - this.parseWithCache(input); - if (!this._parseTree) return null; - - let sqlParserIns = this._parser; - const allTokens = this.getAllTokens(input); - let caretTokenIndex = findCaretTokenIndex(caretPosition, allTokens); - let c3Context: ParserRuleContext = this._parseTree; - let tokenIndexOffset: number = 0; + tokenIndex: number, + originParseTree?: ParserRuleContext | null + ) { + if (arguments.length <= 2) { + this.parseWithCache(input); + originParseTree = this._parseTree; + } - if (!caretTokenIndex && caretTokenIndex !== 0) return null; + if (!originParseTree || !input?.length) return null; + const splitListener = this.splitListener; /** * Split sql by statement. * Try to collect candidates in as small a range as possible. */ - this.listen(splitListener, this._parseTree); + this.listen(splitListener, originParseTree); const statementCount = splitListener.statementsContext?.length; const statementsContext = splitListener.statementsContext; + let tokenIndexOffset = 0; + let sqlParserIns = this._parser; + let parseTree = originParseTree; // If there are multiple statements. if (statementCount > 1) { @@ -305,14 +312,14 @@ export abstract class BasicSQL< const isNextCtxValid = index === statementCount - 1 || !statementsContext[index + 1]?.exception; - if (ctx.stop && ctx.stop.tokenIndex < caretTokenIndex && isPrevCtxValid) { + if (ctx.stop && ctx.stop.tokenIndex < tokenIndex && isPrevCtxValid) { startStatement = ctx; } if ( ctx.start && !stopStatement && - ctx.start.tokenIndex > caretTokenIndex && + ctx.start.tokenIndex > tokenIndex && isNextCtxValid ) { stopStatement = ctx; @@ -329,7 +336,7 @@ export abstract class BasicSQL< * compared to the tokenIndex in the whole input */ tokenIndexOffset = startStatement?.start?.tokenIndex ?? 0; - caretTokenIndex = caretTokenIndex - tokenIndexOffset; + tokenIndex = tokenIndex - tokenIndexOffset; /** * Reparse the input fragment, @@ -349,17 +356,54 @@ export abstract class BasicSQL< parser.errorHandler = new ErrorStrategy(); sqlParserIns = parser; - c3Context = parser.program(); + parseTree = parser.program(); } + return { + parser: sqlParserIns, + parseTree, + tokenIndexOffset, + newTokenIndex: tokenIndex, + }; + } + + /** + * Get suggestions of syntax and token at caretPosition + * @param input source string + * @param caretPosition caret position, such as cursor position + * @returns suggestion + */ + public getSuggestionAtCaretPosition( + input: string, + caretPosition: CaretPosition + ): Suggestions | null { + this.parseWithCache(input); + + if (!this._parseTree) return null; + + const allTokens = this.getAllTokens(input); + let caretTokenIndex = findCaretTokenIndex(caretPosition, allTokens); + + if (!caretTokenIndex && caretTokenIndex !== 0) return null; + + const minimumParser = this.getMinimumParserInfo(input, caretTokenIndex); + + if (!minimumParser) return null; + + const { + parser: sqlParserIns, + tokenIndexOffset, + newTokenIndex, + parseTree: c3Context, + } = minimumParser; const core = new CodeCompletionCore(sqlParserIns); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(caretTokenIndex, c3Context); + const candidates = core.collectCandidates(newTokenIndex, c3Context); const originalSuggestions = this.processCandidates( candidates, allTokens, - caretTokenIndex, + newTokenIndex, tokenIndexOffset ); diff --git a/src/parser/common/parseErrorListener.ts b/src/parser/common/parseErrorListener.ts index 902a36f3..f2b92b72 100644 --- a/src/parser/common/parseErrorListener.ts +++ b/src/parser/common/parseErrorListener.ts @@ -10,8 +10,8 @@ import { InputMismatchException, NoViableAltException, } from 'antlr4ng'; -import { LOCALE_TYPE } from './types'; import { transform } from './transform'; +import { BasicSQL } from './basicSQL'; /** * Converted from {@link SyntaxError}. @@ -48,10 +48,19 @@ export type ErrorListener = (parseError: ParseError, originalError: SyntaxError) export abstract class ParseErrorListener implements ANTLRErrorListener { private _errorListener: ErrorListener; - private locale: LOCALE_TYPE; + protected preferredRules: Set; + protected get locale() { + return this.parserContext.locale; + } + protected parserContext: BasicSQL; - constructor(errorListener: ErrorListener, locale: LOCALE_TYPE = 'en_US') { - this.locale = locale; + constructor( + errorListener: ErrorListener, + parserContext: BasicSQL, + preferredRules: Set + ) { + this.parserContext = parserContext; + this.preferredRules = preferredRules; this._errorListener = errorListener; } diff --git a/src/parser/flink/flinkErrorListener.ts b/src/parser/flink/flinkErrorListener.ts index 61a8a157..bc068aa2 100644 --- a/src/parser/flink/flinkErrorListener.ts +++ b/src/parser/flink/flinkErrorListener.ts @@ -1,12 +1,9 @@ import { CodeCompletionCore } from 'antlr4-c3'; -import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; +import { 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; - private objectNames: Map = new Map([ [FlinkSqlParser.RULE_catalogPath, 'catalog'], [FlinkSqlParser.RULE_catalogPathCreate, 'catalog'], @@ -22,22 +19,34 @@ export class FlinkErrorListener extends ParseErrorListener { [FlinkSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result: string[] = []; diff --git a/src/parser/flink/index.ts b/src/parser/flink/index.ts index 61d19d22..b31e9338 100644 --- a/src/parser/flink/index.ts +++ b/src/parser/flink/index.ts @@ -39,8 +39,9 @@ export class FlinkSQL extends BasicSQL; - private objectNames: Map = new Map([ [HiveSqlParser.RULE_dbSchemaName, 'database'], [HiveSqlParser.RULE_dbSchemaNameCreate, 'database'], @@ -21,22 +18,34 @@ export class HiveErrorListener extends ParseErrorListener { [HiveSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result: string[] = []; diff --git a/src/parser/hive/index.ts b/src/parser/hive/index.ts index 7bac2468..96a954aa 100644 --- a/src/parser/hive/index.ts +++ b/src/parser/hive/index.ts @@ -40,8 +40,9 @@ export class HiveSQL extends BasicSQL; - private objectNames: Map = new Map([ [ImpalaSqlParser.RULE_databaseNamePath, 'database'], [ImpalaSqlParser.RULE_databaseNameCreate, 'database'], @@ -20,22 +17,34 @@ export class ImpalaErrorListener extends ParseErrorListener { [ImpalaSqlParser.RULE_columnNamePathCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result: string[] = []; diff --git a/src/parser/impala/index.ts b/src/parser/impala/index.ts index 9902507e..3f19b8d4 100644 --- a/src/parser/impala/index.ts +++ b/src/parser/impala/index.ts @@ -38,8 +38,9 @@ export class ImpalaSQL extends BasicSQL { return new MysqlSplitListener(); } - protected createErrorListener(_errorListener: ErrorListener) { - return new MysqlErrorListener(_errorListener, this.preferredRules, this.locale); + protected createErrorListener(_errorListener: ErrorListener): MysqlErrorListener { + const parserContext = this; + return new MysqlErrorListener(_errorListener, parserContext, this.preferredRules); } protected createEntityCollector(input: string, caretTokenIndex?: number) { diff --git a/src/parser/mysql/mysqlErrorListener.ts b/src/parser/mysql/mysqlErrorListener.ts index ba6c1440..c015ee5a 100644 --- a/src/parser/mysql/mysqlErrorListener.ts +++ b/src/parser/mysql/mysqlErrorListener.ts @@ -1,12 +1,9 @@ import { CodeCompletionCore } from 'antlr4-c3'; -import { ErrorListener, ParseErrorListener } from '../common/parseErrorListener'; +import { 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; - private objectNames: Map = new Map([ [MySqlParser.RULE_databaseName, 'database'], [MySqlParser.RULE_databaseNameCreate, 'database'], @@ -20,22 +17,34 @@ export class MysqlErrorListener extends ParseErrorListener { [MySqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result: string[] = []; diff --git a/src/parser/postgresql/index.ts b/src/parser/postgresql/index.ts index 63820000..2aa2c68e 100644 --- a/src/parser/postgresql/index.ts +++ b/src/parser/postgresql/index.ts @@ -43,8 +43,9 @@ export class PostgreSQL extends BasicSQL; - private objectNames: Map = new Map([ [PostgreSqlParser.RULE_database_name, 'database'], [PostgreSqlParser.RULE_database_name_create, 'database'], @@ -24,22 +21,34 @@ export class PostgreSqlErrorListener extends ParseErrorListener { [PostgreSqlParser.RULE_procedure_name, 'procedure'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result: string[] = []; diff --git a/src/parser/spark/index.ts b/src/parser/spark/index.ts index 1b847fd8..c6c54672 100644 --- a/src/parser/spark/index.ts +++ b/src/parser/spark/index.ts @@ -38,8 +38,9 @@ export class SparkSQL extends BasicSQL; - private objectNames: Map = new Map([ [SparkSqlParser.RULE_namespaceName, 'namespace'], [SparkSqlParser.RULE_namespaceNameCreate, 'namespace'], @@ -20,23 +17,34 @@ export class SparkErrorListener extends ParseErrorListener { [SparkSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result = []; // get expectedText as collect rules first diff --git a/src/parser/trino/index.ts b/src/parser/trino/index.ts index cf1f9db4..8eab81c4 100644 --- a/src/parser/trino/index.ts +++ b/src/parser/trino/index.ts @@ -25,8 +25,9 @@ export class TrinoSQL extends BasicSQL; - private objectNames: Map = new Map([ [TrinoSqlParser.RULE_catalogRef, 'catalog'], [TrinoSqlParser.RULE_catalogNameCreate, 'catalog'], @@ -22,22 +19,34 @@ export class TrinoErrorListener extends ParseErrorListener { [TrinoSqlParser.RULE_columnNameCreate, 'column'], ]); - constructor(errorListener: ErrorListener, preferredRules: Set, locale: LOCALE_TYPE) { - super(errorListener, locale); - this.preferredRules = preferredRules; - } - public getExpectedText(parser: Parser, token: Token) { let expectedText = ''; + const input = this.parserContext.getParsedInput(); + /** + * Get the program context. + * When called error listener, `this._parseTree` is still `undefined`, + * so we can't use cached parseTree in `getMinimumParserInfo` + */ let currentContext = parser.context ?? undefined; while (currentContext?.parent) { currentContext = currentContext.parent; } - const core = new CodeCompletionCore(parser); + const parserInfo = this.parserContext.getMinimumParserInfo( + input, + token.tokenIndex, + currentContext + ); + + if (!parserInfo) return ''; + + const { parser: c3Parser, newTokenIndex, parseTree: c3Context } = parserInfo; + + const core = new CodeCompletionCore(c3Parser); core.preferredRules = this.preferredRules; - const candidates = core.collectCandidates(token.tokenIndex, currentContext); + + const candidates = core.collectCandidates(newTokenIndex, c3Context); if (candidates.rules.size) { const result: string[] = [];