diff --git a/packages/kbn-esql-ast/README.md b/packages/kbn-esql-ast/README.md index dcb244af3c381..b4f865129bca2 100644 --- a/packages/kbn-esql-ast/README.md +++ b/packages/kbn-esql-ast/README.md @@ -5,14 +5,32 @@ building, traversal, pretty-printing, and manipulation features on top of a custom compact AST representation, which is designed to be resilient to many grammar changes. -Contents of this package: -- [`builder` — Contains the `Builder` class for AST node construction](./src/builder/README.md). +### Contents of the package + +At the lowest level, the package provides a parser that converts ES|QL text into +an AST representation. Or, you can use the `Builder` class to construct the AST +manually: + - [`parser` — Contains text to ES|QL AST parsing code](./src/parser/README.md). +- [`builder` — Contains the `Builder` class for AST node construction](./src/builder/README.md). + +The *Traversal API* lets you walk the AST. The `Walker` class is a simple +to use, but the `Visitor` class is more powerful and flexible: + - [`walker` — Contains the ES|QL AST `Walker` utility](./src/walker/README.md). - [`visitor` — Contains the ES|QL AST `Visitor` utility](./src/visitor/README.md). + +Higher-level functionality is provided by the `mutate` and `synth` modules. They +allow you to traverse and modify the AST, or to easily construct AST nodes: + +- [`mutate` — Contains code for traversing and mutating the AST](./src/mutate/README.md). +- [`synth` — Ability to construct AST nodes from template strings](./src/synth/README.md). + +The *Pretty-printing API* lets you format the AST to text. There are two +implementations — a basic pretty-printer and a wrapping pretty-printer: + - [`pretty_print` — Contains code for formatting AST to text](./src/pretty_print/README.md). -- [`mutate` — Contains code for traversing and mutating the AST.](./src/mutate/README.md). ## Demo diff --git a/packages/kbn-esql-ast/index.ts b/packages/kbn-esql-ast/index.ts index 1780b75f29237..d7254f9de51ba 100644 --- a/packages/kbn-esql-ast/index.ts +++ b/packages/kbn-esql-ast/index.ts @@ -45,6 +45,7 @@ export { } from './src/parser'; export { Walker, type WalkerOptions, walk } from './src/walker'; +export * as synth from './src/synth'; export { LeafPrinter, diff --git a/packages/kbn-esql-ast/src/synth/README.md b/packages/kbn-esql-ast/src/synth/README.md new file mode 100644 index 0000000000000..48f0cee2dc70b --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/README.md @@ -0,0 +1,124 @@ +# ES|QL `synth` module + +The `synth` module lets you easily "synthesize" AST nodes from template strings. +This is useful when you need to construct AST nodes programmatically, but don't +want to deal with the complexity of the `Builder` class. + + +## Usage + +You can create an assignment expression AST node as simle as: + +```ts +import { synth } from '@kbn/esql-ast'; + +const node = synth.expr `my.field = max(10, ?my_param)`; +// { type: 'function', name: '=', args: [ ... ]} +``` + +To construct an equivalent AST node using the `Builder` class, you would need to +write the following code: + +```ts +import { Builder } from '@kbn/esql-ast'; + +const node = Builder.expression.func.binary('=', [ + Builder.expression.column({ + args: [Builder.identifier({ name: 'my' }), Builder.identifier({ name: 'field' })], + }), + Builder.expression.func.call('max', [ + Builder.expression.literal.integer(10), + Builder.param.named({ value: 'my_param' }), + ]), +]); +// { type: 'function', name: '=', args: [ ... ]} +``` + +You can nest template strings to create more complex AST nodes: + +```ts +const field = synth.expr `my.field`; +const value = synth.expr `max(10, ?my_param)`; + +const assignment = synth.expr`${field} = ${value}`; +// { type: 'function', name: '=', args: [ +// { type: 'column', args: [ ... ] }, +// { type: 'function', name: 'max', args: [ ... ] } +// ]} +``` + +Use the `synth.cmd` method to create command nodes: + +```ts +const command = synth.cmd `WHERE my.field == 10`; +// { type: 'command', name: 'WHERE', args: [ ... ]} +``` + +AST nodes created by the synthesizer are pretty-printed when you coerce them to +a string or call the `toString` method: + +```ts +const command = synth.cmd ` WHERE my.field == 10 `; // { type: 'command', ... } +String(command); // "WHERE my.field == 10" +``` + + +## Reference + +### `synth.expr` + +The `synth.expr` synthesizes an expression AST nodes. (*Expressions* are +basically any thing that can go into a `WHERE` command, like boolean expression, +function call, literal, params, etc.) + +Use it as a function: + +```ts +const node = synth.expr('my.field = max(10, ?my_param)'); +``` + +Specify parser options: + +```ts +const node = synth.expr('my.field = max(10, ?my_param)', + { withFormatting: false }); +``` + +Use it as a template string tag: + +```ts +const node = synth.expr `my.field = max(10, ?my_param)`; +``` + +Specify parser options, when using as a template string tag: + +```ts +const node = synth.expr({ withFormatting: false }) `my.field = max(10, 20)`; +``` + +Combine nodes using template strings: + +```ts +const field = synth.expr `my.field`; +const node = synth.expr `${field} = max(10, 20)`; +``` + +Print the node as a string: + +```ts +const node = synth.expr `my.field = max(10, 20)`; +String(node); // 'my.field = max(10, 20)' +``` + + +### `synth.cmd` + +The `synth.cmd` synthesizes a command AST node (such as `SELECT`, `WHERE`, +etc.). You use it the same as the `synth.expr` function or template string tag. +The only difference is that the `synth.cmd` function or tag creates a command +AST node. + +```ts +const node = synth.cmd `WHERE my.field == 10`; +// { type: 'command', name: 'where', args: [ ... ]} +``` diff --git a/packages/kbn-esql-ast/src/synth/__tests__/cmd.test.ts b/packages/kbn-esql-ast/src/synth/__tests__/cmd.test.ts new file mode 100644 index 0000000000000..1158c2bf838e0 --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/__tests__/cmd.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BasicPrettyPrinter } from '../../pretty_print'; +import { cmd } from '../cmd'; +import { expr } from '../expr'; + +test('can create a WHERE command', () => { + const node = cmd`WHERE coordinates.lat >= 12.123123`; + const text = BasicPrettyPrinter.command(node); + + expect(text).toBe('WHERE coordinates.lat >= 12.123123'); +}); + +test('can create a complex STATS command', () => { + const node = cmd`STATS count_last_hour = SUM(count_last_hour), total_visits = SUM(total_visits), bytes_transform = SUM(bytes_transform), bytes_transform_last_hour = SUM(bytes_transform_last_hour) BY extension.keyword`; + const text = BasicPrettyPrinter.command(node); + + expect(text).toBe( + 'STATS count_last_hour = SUM(count_last_hour), total_visits = SUM(total_visits), bytes_transform = SUM(bytes_transform), bytes_transform_last_hour = SUM(bytes_transform_last_hour) BY extension.keyword' + ); +}); + +test('can create a FROM source command', () => { + const node = cmd`FROM index METADATA _id`; + const text = BasicPrettyPrinter.command(node); + + expect(text).toBe('FROM index METADATA _id'); +}); + +test('throws if specified source is not a command', () => { + expect(() => cmd`123`).toThrowError(); +}); + +test('can compose expressions into commands', () => { + const field = expr`a.b.c`; + const cmd1 = cmd` WHERE ${field} == "asdf"`; + const cmd2 = cmd` DISSECT ${field} """%{date}"""`; + const text1 = BasicPrettyPrinter.command(cmd1); + const text2 = BasicPrettyPrinter.command(cmd2); + + expect(text1).toBe('WHERE a.b.c == "asdf"'); + expect(text2).toBe('DISSECT a.b.c """%{date}"""'); +}); diff --git a/packages/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts b/packages/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts new file mode 100644 index 0000000000000..a859fbc2c0d3d --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BasicPrettyPrinter } from '../../pretty_print'; +import { ESQLProperNode } from '../../types'; +import { Walker } from '../../walker/walker'; +import { expr } from '../expr'; + +test('can generate integer literal', () => { + const node = expr('42'); + + expect(node).toMatchObject({ + type: 'literal', + literalType: 'integer', + name: '42', + value: 42, + }); +}); + +test('can generate integer literal and keep comment', () => { + const node = expr('42 /* my 42 */'); + + expect(node).toMatchObject({ + type: 'literal', + literalType: 'integer', + value: 42, + formatting: { + right: [ + { + type: 'comment', + subtype: 'multi-line', + text: ' my 42 ', + }, + ], + }, + }); +}); + +test('can generate a function call expression', () => { + const node = expr('fn(1, "test")'); + + expect(node).toMatchObject({ + type: 'function', + name: 'fn', + args: [ + { + type: 'literal', + literalType: 'integer', + value: 1, + }, + { + type: 'literal', + literalType: 'keyword', + value: '"test"', + }, + ], + }); +}); + +test('can generate assignment expression', () => { + const src = 'a.b.c = AGG(123)'; + const node = expr(src); + const text = BasicPrettyPrinter.expression(node); + + expect(text).toBe(src); +}); + +test('can generate comparison expression', () => { + const src = 'a.b.c >= FN(123)'; + const node = expr(src); + const text = BasicPrettyPrinter.expression(node); + + expect(text).toBe(src); +}); + +describe('can generate various expression types', () => { + const cases: Array<[name: string, src: string]> = [ + ['integer', '42'], + ['negative integer', '-24'], + ['zero', '0'], + ['float', '3.14'], + ['negative float', '-1.23'], + ['string', '"doge"'], + ['empty string', '""'], + ['integer list', '[1, 2, 3]'], + ['string list', '["a", "b"]'], + ['boolean list', '[TRUE, FALSE]'], + ['time interval', '1d'], + ['cast', '"doge"::INTEGER'], + ['addition', '1 + 2'], + ['multiplication', '2 * 2'], + ['parens', '2 * (2 + 3)'], + ['star function call', 'FN(*)'], + ['function call with one argument', 'FN(1)'], + ['nested function calls', 'FN(1, MAX("asdf"))'], + ['basic field', 'col'], + ['nested field', 'a.b.c'], + ['unnamed param', '?'], + ['named param', '?hello'], + ['positional param', '?123'], + ['param in nested field', 'a.?b.c'], + ['escaped field', '`😎`'], + ['nested escaped field', 'emoji.`😎`'], + ['simple assignment', 't = NOW()'], + ['assignment expression', 'bytes_transform = ROUND(total_bytes / 1000000.0, 1)'], + [ + 'assignment with time intervals', + 'key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour), "Last hour", "Other")', + ], + [ + 'assignment with casts', + 'total_visits = TO_DOUBLE(COALESCE(count_last_hour, 0::LONG) + COALESCE(count_rest, 0::LONG))', + ], + ]; + + for (const [name, src] of cases) { + test(name, () => { + const node = expr(src); + const text = BasicPrettyPrinter.expression(node); + + expect(text).toBe(src); + }); + } +}); + +test('parser fields are empty', () => { + const src = 'a.b.c >= FN(123)'; + const ast = expr(src); + + const assertParserFields = (node: ESQLProperNode) => { + expect(node.location.min).toBe(0); + expect(node.location.max).toBe(0); + expect(node.text).toBe(''); + expect(node.incomplete).toBe(false); + }; + + Walker.walk(ast, { + visitAny: (node: ESQLProperNode) => { + assertParserFields(node); + }, + }); +}); diff --git a/packages/kbn-esql-ast/src/synth/__tests__/expr_template.test.ts b/packages/kbn-esql-ast/src/synth/__tests__/expr_template.test.ts new file mode 100644 index 0000000000000..0c6e78a143106 --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/__tests__/expr_template.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BasicPrettyPrinter } from '../../pretty_print'; +import { expr } from '../expr'; + +test('can be used as templated string tag', () => { + const node = expr`42`; + + expect(node).toMatchObject({ + type: 'literal', + literalType: 'integer', + name: '42', + value: 42, + }); +}); + +test('can specify parsing options', () => { + const node1 = expr({ withFormatting: true })`42 /* comment */`; + const node2 = expr({ withFormatting: false })`42 /* comment */`; + const text1 = BasicPrettyPrinter.expression(node1); + const text2 = BasicPrettyPrinter.expression(node2); + + expect(text1).toBe('42 /* comment */'); + expect(text2).toBe('42'); +}); + +test('can compose nodes into templated string', () => { + const field = expr`a.b.c`; + const value = expr`fn(1, ${field})`; + const assignment = expr`${field} = ${value}`; + const text = BasicPrettyPrinter.expression(assignment); + + expect(text).toBe('a.b.c = FN(1, a.b.c)'); +}); diff --git a/packages/kbn-esql-ast/src/synth/__tests__/scenarios.test.ts b/packages/kbn-esql-ast/src/synth/__tests__/scenarios.test.ts new file mode 100644 index 0000000000000..4118d15093d6a --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/__tests__/scenarios.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BasicPrettyPrinter, Builder, synth } from '../../..'; +import { SynthNode } from '../helpers'; + +test('synthesized nodes have SynthNodePrototype prototype', () => { + const expression = synth.expr`?my_param`; + const command = synth.cmd`LIMIT 123`; + + expect(expression).toBeInstanceOf(SynthNode); + expect(command).toBeInstanceOf(SynthNode); +}); + +test('can cast expression to string', () => { + const expression = synth.expr`?my_param`; + + expect(expression).toMatchObject({ + type: 'literal', + literalType: 'param', + paramType: 'named', + value: 'my_param', + }); + expect(String(expression)).toBe('?my_param'); +}); + +test('can build the same expression with Builder', () => { + const expression1 = synth.expr`my.field = max(10, ?my_param)`; + const expression2 = Builder.expression.func.binary('=', [ + Builder.expression.column({ + args: [Builder.identifier({ name: 'my' }), Builder.identifier({ name: 'field' })], + }), + Builder.expression.func.call('max', [ + Builder.expression.literal.integer(10), + Builder.param.named({ value: 'my_param' }), + ]), + ]); + + const expected = 'my.field = MAX(10, ?my_param)'; + + expect(expression1 + '').toBe(expected); + expect(BasicPrettyPrinter.expression(expression1)).toBe(expected); + expect(BasicPrettyPrinter.expression(expression2)).toBe(expected); +}); diff --git a/packages/kbn-esql-ast/src/synth/cmd.ts b/packages/kbn-esql-ast/src/synth/cmd.ts new file mode 100644 index 0000000000000..e43b0e597ccb4 --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/cmd.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ParseOptions } from '../parser'; +import { EsqlQuery } from '../query'; +import { makeSynthNode, createSynthMethod } from './helpers'; +import type { SynthGenerator } from './types'; +import type { ESQLCommand } from '../types'; + +const generator: SynthGenerator = ( + src: string, + { withFormatting = true, ...rest }: ParseOptions = {} +): ESQLCommand => { + src = src.trimStart(); + + const isSourceCommand = /^FROM/i.test(src); + const querySrc = isSourceCommand ? src : 'FROM a | ' + src; + const query = EsqlQuery.fromSrc(querySrc, { withFormatting, ...rest }); + const command = query.ast.commands[isSourceCommand ? 0 : 1]; + + if (command.type !== 'command') { + throw new Error('Expected a command node'); + } + + makeSynthNode(command); + + return command; +}; + +export const cmd = createSynthMethod(generator); diff --git a/packages/kbn-esql-ast/src/synth/expr.ts b/packages/kbn-esql-ast/src/synth/expr.ts new file mode 100644 index 0000000000000..9e56bcbe3c763 --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/expr.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ParseOptions } from '../parser'; +import { EsqlQuery } from '../query'; +import { firstItem } from '../visitor/utils'; +import { makeSynthNode, createSynthMethod } from './helpers'; +import type { SynthGenerator } from './types'; +import type { ESQLAstExpression } from '../types'; + +const generator: SynthGenerator = ( + src: string, + { withFormatting = true, ...rest }: ParseOptions = {} +): ESQLAstExpression => { + const querySrc = 'FROM a | STATS ' + src; + const query = EsqlQuery.fromSrc(querySrc, { withFormatting, ...rest }); + const where = query.ast.commands[1]; + const expression = firstItem(where.args)!; + + makeSynthNode(expression); + + return expression; +}; + +export const expr = createSynthMethod(generator); diff --git a/packages/kbn-esql-ast/src/synth/helpers.ts b/packages/kbn-esql-ast/src/synth/helpers.ts new file mode 100644 index 0000000000000..77419486ec7ce --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/helpers.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Builder } from '../builder'; +import { Walker, WalkerAstNode } from '../walker/walker'; +import { BasicPrettyPrinter } from '../pretty_print'; +import type { ESQLProperNode } from '../types'; +import type { SynthGenerator, SynthMethod, SynthTaggedTemplateWithOpts } from './types'; +import type { ParseOptions } from '../parser'; + +const serialize = (node: ESQLProperNode): string => { + return node.type === 'command' + ? BasicPrettyPrinter.command(node) + : BasicPrettyPrinter.expression(node); +}; + +/** + * This is used as a prototype of AST nodes created by the synth methods. + * It implements the `toString` method, which is invoked when the node is + * coerced to a string. So you can easily convert the node to a string by + * calling `String(node)` or `${node}`: + * + * ```js + * const node = expr`a.b`; // { type: 'column', name: 'a.b' } + * String(node) // 'a.b' + * ``` + */ +export class SynthNode { + toString(this: ESQLProperNode) { + return serialize(this); + } +} + +export const makeSynthNode = (ast: WalkerAstNode) => { + // Add SynthNode prototype to the AST node. + Object.setPrototypeOf(ast, new SynthNode()); + + // Remove parser generated fields. + Walker.walk(ast, { + visitAny: (node) => { + Object.assign(node, Builder.parserFields({})); + }, + }); +}; + +export const createSynthMethod = ( + generator: SynthGenerator +): SynthMethod => { + const templateStringTag: SynthTaggedTemplateWithOpts = ((opts?: ParseOptions) => { + return (template: TemplateStringsArray, ...params: Array) => { + let src = ''; + const length = template.length; + for (let i = 0; i < length; i++) { + src += template[i]; + if (i < params.length) { + const param = params[i]; + if (typeof param === 'string') src += param; + else src += serialize(param); + } + } + return generator(src, opts); + }; + }) as SynthTaggedTemplateWithOpts; + + const method: SynthMethod = ((...args) => { + const [first] = args; + + /** + * Usage as function: + * + * ```js + * expr('42'); + * ``` + */ + if (typeof first === 'string') return generator(first, args[1] as ParseOptions); + + /** + * Usage as tagged template: + * + * ```js + * expr`42`; + * ``` + */ + if (Array.isArray(first)) { + return templateStringTag()( + first as unknown as TemplateStringsArray, + ...(args as any).slice(1) + ); + } + + /** + * Usage as tagged template, with ability to specify parsing options: + * + * ```js + * expr({ withFormatting: false })`42`; + * ``` + */ + return templateStringTag(args[0] as ParseOptions); + }) as SynthMethod; + + return method; +}; diff --git a/packages/kbn-esql-ast/src/synth/index.ts b/packages/kbn-esql-ast/src/synth/index.ts new file mode 100644 index 0000000000000..965828abf0c1d --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { expr } from './expr'; +export { cmd } from './cmd'; diff --git a/packages/kbn-esql-ast/src/synth/types.ts b/packages/kbn-esql-ast/src/synth/types.ts new file mode 100644 index 0000000000000..46f24e70151d4 --- /dev/null +++ b/packages/kbn-esql-ast/src/synth/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ParseOptions } from '../parser'; +import type { ESQLAstExpression, ESQLProperNode } from '../types'; + +export type SynthGenerator = (src: string, opts?: ParseOptions) => N; + +export type SynthTaggedTemplate = ( + template: TemplateStringsArray, + ...params: Array +) => N; + +export type SynthTaggedTemplateWithOpts = ( + opts?: ParseOptions +) => SynthTaggedTemplate; + +export type SynthMethod = SynthGenerator & + SynthTaggedTemplate & + SynthTaggedTemplateWithOpts; diff --git a/packages/kbn-esql-ast/src/visitor/__tests__/scenarios.test.ts b/packages/kbn-esql-ast/src/visitor/__tests__/scenarios.test.ts index ef3a7e6ecf3f4..0b2b5e4a4cb5c 100644 --- a/packages/kbn-esql-ast/src/visitor/__tests__/scenarios.test.ts +++ b/packages/kbn-esql-ast/src/visitor/__tests__/scenarios.test.ts @@ -15,7 +15,7 @@ */ import { parse } from '../../parser'; -import { ESQLAstQueryExpression } from '../../types'; +import { ESQLAstItem, ESQLAstQueryExpression } from '../../types'; import { Visitor } from '../visitor'; test('change LIMIT from 24 to 42', () => { @@ -93,7 +93,7 @@ test('can remove a specific WHERE command', () => { }) .on('visitCommand', (ctx) => { if (ctx.node.name === 'where') { - ctx.node.args = [...ctx.visitArguments()].filter(Boolean); + ctx.node.args = [...ctx.visitArguments()].filter(Boolean) as ESQLAstItem[]; } return ctx.node; }) diff --git a/packages/kbn-esql-ast/src/visitor/contexts.ts b/packages/kbn-esql-ast/src/visitor/contexts.ts index 086a217d8f117..e1570f7143c07 100644 --- a/packages/kbn-esql-ast/src/visitor/contexts.ts +++ b/packages/kbn-esql-ast/src/visitor/contexts.ts @@ -15,6 +15,7 @@ import { type GlobalVisitorContext, SharedData } from './global_visitor_context' import { children, firstItem, singleItems } from './utils'; import type { ESQLAstCommand, + ESQLAstExpression, ESQLAstItem, ESQLAstNodeWithArgs, ESQLAstNodeWithChildren, @@ -28,7 +29,6 @@ import type { ESQLList, ESQLLiteral, ESQLOrderExpression, - ESQLSingleAstItem, ESQLSource, ESQLTimeInterval, } from '../types'; @@ -541,7 +541,7 @@ export class InlineCastExpressionVisitorContext< Methods extends VisitorMethods = VisitorMethods, Data extends SharedData = SharedData > extends ExpressionVisitorContext { - public value(): ESQLSingleAstItem { + public value(): ESQLAstExpression { this.ctx.assertMethodExists('visitExpression'); const value = firstItem([this.node.value])!; diff --git a/packages/kbn-esql-ast/src/visitor/types.ts b/packages/kbn-esql-ast/src/visitor/types.ts index 0ba7dea503a8f..6dd49eabb013e 100644 --- a/packages/kbn-esql-ast/src/visitor/types.ts +++ b/packages/kbn-esql-ast/src/visitor/types.ts @@ -20,7 +20,7 @@ export type ESQLAstQueryNode = ast.ESQLAstQueryExpression; * Represents an "expression" node in the AST. */ // export type ESQLAstExpressionNode = ESQLAstItem; -export type ESQLAstExpressionNode = ast.ESQLSingleAstItem; +export type ESQLAstExpressionNode = ast.ESQLAstExpression; /** * All possible AST nodes supported by the visitor. diff --git a/packages/kbn-esql-ast/src/visitor/utils.ts b/packages/kbn-esql-ast/src/visitor/utils.ts index da8544ef46c90..00b7a4541ccdd 100644 --- a/packages/kbn-esql-ast/src/visitor/utils.ts +++ b/packages/kbn-esql-ast/src/visitor/utils.ts @@ -7,14 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLAstItem, ESQLProperNode, ESQLSingleAstItem } from '../types'; +import { ESQLAstExpression, ESQLAstItem, ESQLProperNode, ESQLSingleAstItem } from '../types'; /** * Normalizes AST "item" list to only contain *single* items. * * @param items A list of single or nested items. */ -export function* singleItems(items: Iterable): Iterable { +export function* singleItems( + items: Iterable +): Iterable { for (const item of items) { if (Array.isArray(item)) { yield* singleItems(item); @@ -30,7 +32,7 @@ export function* singleItems(items: Iterable): Iterable { +export const firstItem = (items: ESQLAstItem[]): ESQLAstExpression | undefined => { for (const item of singleItems(items)) { return item; } @@ -53,7 +55,7 @@ export const lastItem = (items: ESQLAstItem[]): ESQLSingleAstItem | undefined => return last as ESQLSingleAstItem; }; -export function* children(node: ESQLProperNode): Iterable { +export function* children(node: ESQLProperNode): Iterable { switch (node.type) { case 'function': case 'command':