mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ES|QL] Refactor parser factory module (#221245)
## Summary - Cleans up the `parser.ts` module, where ES|QL ANTLR parser is constructed. - Create a distinct `Parser` class, which will hold all parsing related logic. - Removes some unnecessary imports and deprecates more imports. - ANTLR parser and ANTLR lexer are now internal - This sets up for removal of the `ESQLAstBuilderListener` class, which seems to be completely unnecessary and actually counterproductive in our parser. ANTLR has two CST traversal methods (listener and manual), this listener class is used only to traverse the top level (commands), but most of our parsing is then done using manual traversal. It is inconsistent and actually bad, because this way we cannot support nested sub-queries. ## New API Create a parser: ```ts const parser = Parser.create(src); ``` Parse ES|QL AST from src: ```ts const { root } = Parser.parse(src); ``` Get parsing errors only: ```ts const errors = Parser.parseErrors(src); ```
This commit is contained in:
parent
e1bbcc5c6b
commit
a4940f5a78
3 changed files with 116 additions and 115 deletions
|
@ -53,7 +53,6 @@ export { Builder, type AstNodeParserFields, type AstNodeTemplate } from './src/b
|
|||
|
||||
export {
|
||||
createParser,
|
||||
getLexer,
|
||||
parse,
|
||||
parseErrors,
|
||||
type ParseOptions,
|
||||
|
|
|
@ -7,13 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export {
|
||||
getLexer,
|
||||
createParser,
|
||||
parse,
|
||||
parseErrors,
|
||||
type ParseOptions,
|
||||
type ParseResult,
|
||||
} from './parser';
|
||||
export { createParser, parse, parseErrors, type ParseOptions, type ParseResult } from './parser';
|
||||
|
||||
export { ESQLErrorListener } from './esql_error_listener';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { CharStreams, type Token } from 'antlr4';
|
||||
import { CommonTokenStream, type CharStream, type ErrorListener } from 'antlr4';
|
||||
import { CommonTokenStream, type CharStream } from 'antlr4';
|
||||
import { ESQLErrorListener } from './esql_error_listener';
|
||||
import { ESQLAstBuilderListener } from './esql_ast_builder_listener';
|
||||
import { GRAMMAR_ROOT_RULE } from './constants';
|
||||
|
@ -17,7 +17,6 @@ import type { ESQLAst, ESQLAstQueryExpression, EditorError } from '../types';
|
|||
import { Builder } from '../builder';
|
||||
import { default as ESQLLexer } from '../antlr/esql_lexer';
|
||||
import { default as ESQLParser } from '../antlr/esql_parser';
|
||||
import { default as ESQLParserListener } from '../antlr/esql_parser_listener';
|
||||
|
||||
/**
|
||||
* Some changes to the grammar deleted the literal names for some tokens.
|
||||
|
@ -42,49 +41,6 @@ const replaceSymbolsWithLiterals = (
|
|||
}
|
||||
};
|
||||
|
||||
export const getLexer = (inputStream: CharStream, errorListener: ErrorListener<any>) => {
|
||||
const lexer = new ESQLLexer(inputStream);
|
||||
replaceSymbolsWithLiterals(lexer.symbolicNames, lexer.literalNames);
|
||||
|
||||
lexer.removeErrorListeners();
|
||||
lexer.addErrorListener(errorListener);
|
||||
|
||||
return lexer;
|
||||
};
|
||||
|
||||
const getParser = (
|
||||
inputStream: CharStream,
|
||||
errorListener: ErrorListener<any>,
|
||||
parseListener?: ESQLParserListener
|
||||
) => {
|
||||
const lexer = getLexer(inputStream, errorListener);
|
||||
const tokens = new CommonTokenStream(lexer);
|
||||
const parser = new ESQLParser(tokens);
|
||||
replaceSymbolsWithLiterals(parser.symbolicNames, parser.literalNames);
|
||||
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(errorListener);
|
||||
|
||||
if (parseListener) {
|
||||
// @ts-expect-error the addParseListener API does exist and is documented here
|
||||
// https://github.com/antlr/antlr4/blob/dev/doc/listeners.md
|
||||
parser.addParseListener(parseListener);
|
||||
}
|
||||
|
||||
return {
|
||||
lexer,
|
||||
tokens,
|
||||
parser,
|
||||
};
|
||||
};
|
||||
|
||||
export const createParser = (src: string) => {
|
||||
const errorListener = new ESQLErrorListener();
|
||||
const parseListener = new ESQLAstBuilderListener(src);
|
||||
|
||||
return getParser(CharStreams.fromString(src), errorListener, parseListener);
|
||||
};
|
||||
|
||||
export interface ParseOptions {
|
||||
/**
|
||||
* Whether to collect and attach to AST nodes user's custom formatting:
|
||||
|
@ -117,67 +73,120 @@ export interface ParseResult {
|
|||
errors: EditorError[];
|
||||
}
|
||||
|
||||
export const parse = (text: string | undefined, options: ParseOptions = {}): ParseResult => {
|
||||
try {
|
||||
if (text == null) {
|
||||
const commands: ESQLAstQueryExpression['commands'] = [];
|
||||
return { ast: commands, root: Builder.expression.query(commands), errors: [], tokens: [] };
|
||||
export class Parser {
|
||||
public static readonly create = (src: string, options?: ParseOptions) => {
|
||||
return new Parser(src, options);
|
||||
};
|
||||
|
||||
public static readonly parse = (src: string, options?: ParseOptions): ParseResult => {
|
||||
return Parser.create(src, options).parse();
|
||||
};
|
||||
|
||||
public static readonly parseErrors = (src: string): EditorError[] => {
|
||||
return Parser.create(src).parseErrors();
|
||||
};
|
||||
|
||||
public readonly streams: CharStream;
|
||||
public readonly lexer: ESQLLexer;
|
||||
public readonly tokens: CommonTokenStream;
|
||||
public readonly parser: ESQLParser;
|
||||
public readonly errors = new ESQLErrorListener();
|
||||
public readonly listener: ESQLAstBuilderListener;
|
||||
|
||||
constructor(public readonly src: string, public readonly options: ParseOptions = {}) {
|
||||
this.listener = new ESQLAstBuilderListener(src);
|
||||
const streams = (this.streams = CharStreams.fromString(src));
|
||||
const lexer = (this.lexer = new ESQLLexer(streams));
|
||||
const tokens = (this.tokens = new CommonTokenStream(lexer));
|
||||
const parser = (this.parser = new ESQLParser(tokens));
|
||||
|
||||
replaceSymbolsWithLiterals(lexer.symbolicNames, lexer.literalNames);
|
||||
replaceSymbolsWithLiterals(parser.symbolicNames, parser.literalNames);
|
||||
|
||||
lexer.removeErrorListeners();
|
||||
lexer.addErrorListener(this.errors);
|
||||
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(this.errors);
|
||||
|
||||
if (this.listener) {
|
||||
// The addParseListener API does exist and is documented here
|
||||
// https://github.com/antlr/antlr4/blob/dev/doc/listeners.md
|
||||
(parser as unknown as { addParseListener: any }).addParseListener(this.listener);
|
||||
}
|
||||
const errorListener = new ESQLErrorListener();
|
||||
const parseListener = new ESQLAstBuilderListener(text);
|
||||
const { tokens, parser } = getParser(
|
||||
CharStreams.fromString(text),
|
||||
errorListener,
|
||||
parseListener
|
||||
);
|
||||
|
||||
parser[GRAMMAR_ROOT_RULE]();
|
||||
|
||||
const errors = errorListener.getErrors();
|
||||
const { ast: commands } = parseListener.getAst();
|
||||
const root = Builder.expression.query(commands, {
|
||||
location: {
|
||||
min: 0,
|
||||
max: text.length - 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (options.withFormatting) {
|
||||
const decorations = collectDecorations(tokens);
|
||||
attachDecorations(root, tokens.tokens, decorations.lines);
|
||||
}
|
||||
|
||||
return { root, ast: commands, errors, tokens: tokens.tokens };
|
||||
} catch (error) {
|
||||
if (error !== 'Empty Stack')
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
||||
const root = Builder.expression.query();
|
||||
|
||||
return {
|
||||
root,
|
||||
ast: root.commands,
|
||||
errors: [
|
||||
{
|
||||
startLineNumber: 0,
|
||||
endLineNumber: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
message: `Invalid query [${text}]`,
|
||||
severity: 'error',
|
||||
},
|
||||
],
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const parseErrors = (text: string) => {
|
||||
const errorListener = new ESQLErrorListener();
|
||||
const { parser } = getParser(CharStreams.fromString(text), errorListener);
|
||||
|
||||
parser[GRAMMAR_ROOT_RULE]();
|
||||
|
||||
return errorListener.getErrors();
|
||||
|
||||
public parse(): ParseResult {
|
||||
const { src, options } = this;
|
||||
|
||||
try {
|
||||
this.parser[GRAMMAR_ROOT_RULE]();
|
||||
|
||||
const errors = this.errors.getErrors();
|
||||
const { ast: commands } = this.listener.getAst();
|
||||
const root = Builder.expression.query(commands, {
|
||||
location: {
|
||||
min: 0,
|
||||
max: src.length - 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (options.withFormatting) {
|
||||
const decorations = collectDecorations(this.tokens);
|
||||
attachDecorations(root, this.tokens.tokens, decorations.lines);
|
||||
}
|
||||
|
||||
return { root, ast: commands, errors, tokens: this.tokens.tokens };
|
||||
} catch (error) {
|
||||
if (error !== 'Empty Stack')
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
||||
const root = Builder.expression.query();
|
||||
|
||||
return {
|
||||
root,
|
||||
ast: root.commands,
|
||||
errors: [
|
||||
{
|
||||
startLineNumber: 0,
|
||||
endLineNumber: 0,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
message: `Invalid query [${src}]`,
|
||||
severity: 'error',
|
||||
},
|
||||
],
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public parseErrors(): EditorError[] {
|
||||
this.parser[GRAMMAR_ROOT_RULE]();
|
||||
|
||||
return this.errors.getErrors();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `Parser.create` instead.
|
||||
*/
|
||||
export const createParser = Parser.create;
|
||||
|
||||
/**
|
||||
* @deprecated Use `Parser.parseErrors` instead.
|
||||
*/
|
||||
export const parseErrors = Parser.parseErrors;
|
||||
|
||||
/**
|
||||
* @deprecated Use `Parser.parse` instead.
|
||||
*/
|
||||
export const parse = (src: string | undefined, options: ParseOptions = {}): ParseResult => {
|
||||
if (src == null) {
|
||||
const commands: ESQLAstQueryExpression['commands'] = [];
|
||||
return { ast: commands, root: Builder.expression.query(commands), errors: [], tokens: [] };
|
||||
}
|
||||
|
||||
return Parser.create(src, options).parse();
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue