@tellaro/tql 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +67 -64
- package/dist/src/builder.d.ts +6 -0
- package/dist/src/completionProvider.js +276 -195
- package/dist/src/complexityAnalyzer.js +41 -32
- package/dist/src/index.d.ts +2 -2
- package/dist/src/parser.d.ts +193 -1
- package/dist/src/parser.js +614 -127
- package/dist/src/semanticPreview.d.ts +23 -0
- package/dist/src/semanticPreview.js +111 -41
- package/dist/src/tokenizer.d.ts +134 -5
- package/dist/src/tokenizer.js +47 -30
- package/dist/src/types.d.ts +86 -4
- package/dist/src/validator.d.ts +101 -1
- package/dist/src/validator.js +391 -152
- package/dist/tql.css +1 -1
- package/dist/vue/TqlInput.vue.js +1 -1
- package/dist/vue/TqlInput.vue2.js +273 -254
- package/package.json +1 -1
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import { parseTql as
|
|
2
|
-
|
|
1
|
+
import { parseTql as d } from "./parser.js";
|
|
2
|
+
import { MUTATORS as p } from "./tokenizer.js";
|
|
3
|
+
const g = {
|
|
3
4
|
maxDepth: 5,
|
|
4
5
|
maxRegex: 3,
|
|
5
6
|
maxInListSize: 100,
|
|
6
7
|
maxOrChainLength: 10,
|
|
7
8
|
maxLeadingWildcards: 2
|
|
8
|
-
}, h = /* @__PURE__ */ new Set([
|
|
9
|
+
}, h = new Set(p), c = /* @__PURE__ */ new Set([
|
|
9
10
|
"geoip_lookup",
|
|
10
11
|
"geoip",
|
|
11
12
|
"geo",
|
|
12
13
|
"nslookup"
|
|
13
|
-
]),
|
|
14
|
-
|
|
14
|
+
]), y = /\*/g, f = /^\*/;
|
|
15
|
+
function u(o, e, t) {
|
|
16
|
+
const a = c.has(o);
|
|
17
|
+
return {
|
|
18
|
+
type: "post_processing_mutator",
|
|
19
|
+
message: a ? `Mutator '${o}' requires post-processing and may be slow` : `Mutator '${o}' requires post-processing: matching happens after results return, so the filter cannot be pushed into OpenSearch`,
|
|
20
|
+
severity: a ? "warning" : "info",
|
|
21
|
+
location: { start: e, end: t }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
class x {
|
|
15
25
|
thresholds;
|
|
16
26
|
constructor(e) {
|
|
17
|
-
this.thresholds = { ...
|
|
27
|
+
this.thresholds = { ...g, ...e };
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* Analyze a TQL query for complexity
|
|
21
31
|
*/
|
|
22
32
|
analyze(e) {
|
|
23
|
-
const t =
|
|
33
|
+
const t = d(e), a = {
|
|
24
34
|
depth: 0,
|
|
25
35
|
operatorCount: 0,
|
|
26
36
|
wildcardCount: 0,
|
|
@@ -59,6 +69,11 @@ class c {
|
|
|
59
69
|
case "comparison":
|
|
60
70
|
this.analyzeComparisonExpression(e, t, a);
|
|
61
71
|
break;
|
|
72
|
+
// `field ANY value` -- the cost lives in the wrapped comparison (its mutators, its
|
|
73
|
+
// regex, its list size), so recurse rather than scoring the wrapper as nothing.
|
|
74
|
+
case "collection":
|
|
75
|
+
this.analyzeExpression(e.condition, t, a, n, s);
|
|
76
|
+
break;
|
|
62
77
|
case "group":
|
|
63
78
|
this.analyzeGroupExpression(e, t, a, n, s);
|
|
64
79
|
break;
|
|
@@ -79,21 +94,11 @@ class c {
|
|
|
79
94
|
t.operatorCount++;
|
|
80
95
|
const n = e.field;
|
|
81
96
|
if (n.type === "mutator")
|
|
82
|
-
t.mutatorCount++, h.has(n.mutator) && (t.postProcessingMutatorCount++, a.push(
|
|
83
|
-
type: "post_processing_mutator",
|
|
84
|
-
message: `Mutator '${n.mutator}' requires post-processing and may be slow`,
|
|
85
|
-
severity: "warning",
|
|
86
|
-
location: { start: n.start, end: n.end }
|
|
87
|
-
}));
|
|
97
|
+
t.mutatorCount++, h.has(n.mutator) && (t.postProcessingMutatorCount++, a.push(u(n.mutator, n.start, n.end)));
|
|
88
98
|
else if (n.type === "field" && n.mutators) {
|
|
89
99
|
t.mutatorCount += n.mutators.length;
|
|
90
100
|
for (const i of n.mutators)
|
|
91
|
-
h.has(i) && (t.postProcessingMutatorCount++, a.push(
|
|
92
|
-
type: "post_processing_mutator",
|
|
93
|
-
message: `Mutator '${i}' requires post-processing and may be slow`,
|
|
94
|
-
severity: "warning",
|
|
95
|
-
location: { start: n.start, end: n.end }
|
|
96
|
-
}));
|
|
101
|
+
h.has(i) && (t.postProcessingMutatorCount++, a.push(u(i, n.start, n.end)));
|
|
97
102
|
}
|
|
98
103
|
const s = e.operator.toLowerCase();
|
|
99
104
|
if ((s === "regexp" || s === "regex" || s === "matches" || s === "not_regexp" || s === "not_regex" || s === "not_matches") && t.regexCount++, this.analyzeValue(e.value, t, a, e), (s === "in" || s === "not_in") && Array.isArray(e.value)) {
|
|
@@ -105,10 +110,10 @@ class c {
|
|
|
105
110
|
const s = Array.isArray(e) ? e : [e];
|
|
106
111
|
for (const i of s)
|
|
107
112
|
if (i.valueType === "string" && typeof i.value == "string") {
|
|
108
|
-
const l = i.value.match(
|
|
109
|
-
l && (t.wildcardCount += l.length,
|
|
113
|
+
const l = i.value.match(y);
|
|
114
|
+
l && (t.wildcardCount += l.length, f.test(i.value) && (t.leadingWildcardCount++, t.leadingWildcardCount <= this.thresholds.maxLeadingWildcards && a.push({
|
|
110
115
|
type: "leading_wildcard",
|
|
111
|
-
message: `
|
|
116
|
+
message: `'${i.value}' contains '*', which TQL treats as a literal character, not a wildcard - use 'endswith' for a suffix match or 'matches' for a regex`,
|
|
112
117
|
severity: "warning",
|
|
113
118
|
location: { start: n.start, end: n.end }
|
|
114
119
|
})));
|
|
@@ -173,34 +178,38 @@ class c {
|
|
|
173
178
|
*/
|
|
174
179
|
generateSuggestions(e, t, a) {
|
|
175
180
|
const n = new Set(e.map((s) => s.type));
|
|
176
|
-
(n.has("leading_wildcard") || n.has("multiple_wildcards")) && t.push(
|
|
181
|
+
(n.has("leading_wildcard") || n.has("multiple_wildcards")) && t.push(
|
|
182
|
+
"TQL has no wildcard syntax - `*` is matched literally. Use `endswith` for a suffix match, `startswith` for a prefix, `contains` for a substring, or `matches` for a regex."
|
|
183
|
+
), n.has("deep_nesting") && t.push(`Query depth of ${a.depth} may impact performance - consider simplifying`), n.has("multiple_regex") && t.push("Multiple regex operators are slow - consider using `contains` or `startswith` where possible"), n.has("large_in_list") && t.push("Large IN lists can be slow - consider splitting into multiple queries or using a lookup table"), n.has("complex_or_chain") && t.push("Consider using `in` instead of multiple `OR` conditions for the same field"), n.has("post_processing_mutator") && t.push("Mutators like `geoip_lookup` and `nslookup` require post-processing and cannot be optimized by the database"), n.has("nested_negation") && t.push("Simplify nested NOT expressions for better readability and potential optimization");
|
|
177
184
|
}
|
|
178
185
|
/**
|
|
179
186
|
* Calculate overall complexity score based on metrics and warnings
|
|
180
187
|
*/
|
|
181
188
|
calculateScore(e, t) {
|
|
182
189
|
let a = 0;
|
|
183
|
-
e.depth > this.thresholds.maxDepth ? a += 2 : e.depth > 3 && (a += 1), e.regexCount > this.thresholds.maxRegex ? a += 2 : e.regexCount > 1 && (a += 1), e.leadingWildcardCount > this.thresholds.maxLeadingWildcards ? a += 2 : e.leadingWildcardCount > 0 && (a += 1), e.inListMaxSize > this.thresholds.maxInListSize ? a += 2 : e.inListMaxSize > 50 && (a += 1),
|
|
190
|
+
e.depth > this.thresholds.maxDepth ? a += 2 : e.depth > 3 && (a += 1), e.regexCount > this.thresholds.maxRegex ? a += 2 : e.regexCount > 1 && (a += 1), e.leadingWildcardCount > this.thresholds.maxLeadingWildcards ? a += 2 : e.leadingWildcardCount > 0 && (a += 1), e.inListMaxSize > this.thresholds.maxInListSize ? a += 2 : e.inListMaxSize > 50 && (a += 1), a += t.filter(
|
|
191
|
+
(s) => s.type === "post_processing_mutator" && s.severity === "warning"
|
|
192
|
+
).length, e.orChainLength > this.thresholds.maxOrChainLength && (a += 1);
|
|
184
193
|
const n = t.filter((s) => s.severity === "error").length;
|
|
185
194
|
return a += n * 2, a >= 4 ? "high" : a >= 2 ? "medium" : "low";
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
function r(o, e) {
|
|
189
|
-
return new
|
|
198
|
+
return new x(e).analyze(o);
|
|
190
199
|
}
|
|
191
|
-
function
|
|
200
|
+
function L(o, e) {
|
|
192
201
|
return r(o, e).score === "high";
|
|
193
202
|
}
|
|
194
|
-
function
|
|
203
|
+
function z(o, e) {
|
|
195
204
|
return r(o, e).warnings;
|
|
196
205
|
}
|
|
197
|
-
function
|
|
206
|
+
function _(o) {
|
|
198
207
|
return r(o).metrics;
|
|
199
208
|
}
|
|
200
209
|
export {
|
|
201
|
-
|
|
210
|
+
x as ComplexityAnalyzer,
|
|
202
211
|
r as analyzeComplexity,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
_ as getComplexityMetrics,
|
|
213
|
+
z as getComplexityWarnings,
|
|
214
|
+
L as isHighComplexity
|
|
206
215
|
};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
export type { TqlToken, TqlTokenType, TqlExpression, TqlBinaryExpression, TqlUnaryExpression, TqlComparisonExpression, TqlCollectionExpression, TqlMutatorExpression, TqlAggregationExpression, TqlStatsExpression, TqlGroupExpression, TqlLiteral, TqlFieldReference, TqlComparisonOperator, TqlLogicalOperator, TqlCollectionOperator, TqlMutator, TqlAggregationFunction, TqlTypeHint, TqlVisualizationType, TqlVizParams, TqlVizHint, TqlParseResult, TqlParseError, TqlParseWarning, TqlErrorCode, TqlWarningCode, TqlValidationResult, TqlValidationOptions, TqlFieldType, TqlCompletionItem, TqlCompletionKind, TqlCompletionContext, TqlCompletionOptions, TqlFieldSchema, TqlSnippet, TqlInputProps, TqlInputEmits, TqlInputExpose } from './types';
|
|
10
|
-
export { TqlTokenizer, tokenizeTql, tokenizeTqlNoWhitespace, findTokenAtPosition, findTokensInRange, findTokenBefore, findPreviousToken, COMPARISON_OPERATORS, SYMBOL_OPERATORS, NEGATION_SYMBOL_OPERATORS, LOGICAL_OPERATORS, COLLECTION_OPERATORS, MUTATORS, AGGREGATION_FUNCTIONS, TYPE_HINTS, VISUALIZATION_TYPES, KEYWORDS } from './tokenizer';
|
|
10
|
+
export { TqlTokenizer, tokenizeTql, tokenizeTqlNoWhitespace, findTokenAtPosition, findTokensInRange, findTokenBefore, findPreviousToken, COMPARISON_OPERATORS, SYMBOL_OPERATORS, NEGATION_SYMBOL_OPERATORS, LOGICAL_OPERATORS, COLLECTION_OPERATORS, MUTATORS, BOOLEAN_PREDICATE_MUTATORS, BOOLEAN_PREDICATE_MUTATOR_SET, AGGREGATION_FUNCTIONS, TYPE_HINTS, VISUALIZATION_TYPES, KEYWORDS } from './tokenizer';
|
|
11
11
|
export { TqlParser, parseTql, isValidTql, getTqlErrors, formatParseError } from './parser';
|
|
12
|
-
export { TqlValidator, validateTql, isValidTqlSyntax, getTqlIssues, formatValidationResult } from './validator';
|
|
12
|
+
export { TqlValidator, validateTql, isValidTqlSyntax, getTqlIssues, formatValidationResult, MAX_REGEX_PATTERN_LENGTH } from './validator';
|
|
13
13
|
export { getCompletions, getWordAtPosition, getCompletionTriggerCharacters, shouldTriggerCompletion } from './completionProvider';
|
|
14
14
|
export { fuzzyMatch, levenshteinDistance, findClosestMatch, fuzzyFilter, highlightMatch, type FuzzyMatchResult, type FuzzyMatchOptions } from './fuzzyMatcher';
|
|
15
15
|
export { SchemaCache, getSchemaCache, createSchemaCache, getCachedFieldNames, getCachedFieldType, getCachedField, type CachedSchema } from './schemaCache';
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { TqlParseError, TqlParseResult } from './types';
|
|
1
|
+
import { TqlCollectionOperator, TqlParseError, TqlParseResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The canonical WORD spelling of a collection operator, derived from the table above rather
|
|
4
|
+
* than written out a second time.
|
|
5
|
+
*
|
|
6
|
+
* `TqlCollectionExpression` normalises every accepted spelling to `operator` + `negated`, so
|
|
7
|
+
* anything that has to show a collection operator back to the user — the semantic preview is
|
|
8
|
+
* the only such consumer today — has to reverse that. Doing it by reading the table means a
|
|
9
|
+
* spelling added to or removed from the parser cannot leave a renderer describing an operator
|
|
10
|
+
* this package no longer accepts.
|
|
11
|
+
*
|
|
12
|
+
* Symbolic spellings (`!any`) are excluded on purpose: `not_any` is the form both engines
|
|
13
|
+
* accept AND the form `COMPARISON_OPERATORS` carries, which is what makes the rendered string
|
|
14
|
+
* a query the user can paste back.
|
|
15
|
+
*/
|
|
16
|
+
export declare function collectionOperatorSpelling(operator: TqlCollectionOperator, negated: boolean): string;
|
|
2
17
|
export declare class TqlParser {
|
|
3
18
|
private tokens;
|
|
4
19
|
private allTokens;
|
|
@@ -42,6 +57,63 @@ export declare class TqlParser {
|
|
|
42
57
|
* - Optional mutator chain: field | lowercase | trim eq "value"
|
|
43
58
|
*/
|
|
44
59
|
private parseComparison;
|
|
60
|
+
/**
|
|
61
|
+
* The part of a field reference after its name token: `::type` and `| mutator` chains.
|
|
62
|
+
*
|
|
63
|
+
* Split out of `parseComparison` so the operator-first collection form
|
|
64
|
+
* (`ANY field::string | lowercase eq "x"`) reads the same field grammar rather than a
|
|
65
|
+
* reduced copy of it. A second copy is how `field::geo` came to be accepted in one place
|
|
66
|
+
* and rejected in another.
|
|
67
|
+
*/
|
|
68
|
+
private parseFieldTail;
|
|
69
|
+
/**
|
|
70
|
+
* Read one `::type` slot, if the current token opens one. Returns the field with the hint
|
|
71
|
+
* applied and whether a hint name was actually consumed.
|
|
72
|
+
*
|
|
73
|
+
* Called from BOTH sides of the mutator chain so the two spellings share one definition of
|
|
74
|
+
* what may follow `::`.
|
|
75
|
+
*
|
|
76
|
+
* Match on the token's VALUE, not its type. `classifyIdentifier` tests the mutator catalog
|
|
77
|
+
* before the type-hint catalog, so a name in both — `geo` is the live case, exported by
|
|
78
|
+
* TYPE_HINTS and by MUTATORS — arrives here typed "mutator" and was rejected. The package
|
|
79
|
+
* exported `geo` as a valid type hint while its own parser refused `field::geo`. After
|
|
80
|
+
* `::` the only thing a token can be is a type name, so the catalog is the right authority
|
|
81
|
+
* and the lexical class is noise.
|
|
82
|
+
*/
|
|
83
|
+
private readTypeHintSlot;
|
|
84
|
+
/**
|
|
85
|
+
* `ANY field [op] value` — the operator-first collection forms.
|
|
86
|
+
*
|
|
87
|
+
* `grammar.pest:226-231` gives `collection_comparison` four alternatives; this is the two
|
|
88
|
+
* that lead with the operator. The Python parser accepts NEITHER — probed, not inferred:
|
|
89
|
+
* `ANY f eq 'x'` and `ANY f 'x'` both raise `TQLSyntaxError at position 4`. They are
|
|
90
|
+
* therefore parsed AND flagged: see `RUST_ONLY_COLLECTION_MESSAGE`.
|
|
91
|
+
*
|
|
92
|
+
* The current token is the collection operator.
|
|
93
|
+
*/
|
|
94
|
+
private parseCollectionFirst;
|
|
95
|
+
/**
|
|
96
|
+
* `field [not] ANY [op] value`, or null when the token after the field is not a
|
|
97
|
+
* collection operator at all.
|
|
98
|
+
*
|
|
99
|
+
* Only `field ANY value` — no inner comparison operator — is accepted by both engines.
|
|
100
|
+
* `field ANY eq value` is `grammar.pest`'s third alternative and Python rejects it
|
|
101
|
+
* (`f any eq 'x'` -> `TQLSyntaxError at position 9`), so it parses with the divergence
|
|
102
|
+
* warning rather than being silently accepted or silently refused.
|
|
103
|
+
*/
|
|
104
|
+
private tryParseCollectionTail;
|
|
105
|
+
/**
|
|
106
|
+
* The shared tail of both collection forms: an OPTIONAL inner comparison operator
|
|
107
|
+
* (defaulting to `eq`, as both grammars do) followed by a value or a value list.
|
|
108
|
+
*/
|
|
109
|
+
private finishCollection;
|
|
110
|
+
/**
|
|
111
|
+
* `<field> <comparison-op> <value>` — everything after the field reference.
|
|
112
|
+
*
|
|
113
|
+
* `fieldToken` is threaded through only for the error messages, which name the field the
|
|
114
|
+
* user typed rather than the normalised reference.
|
|
115
|
+
*/
|
|
116
|
+
private parseComparisonTail;
|
|
45
117
|
/**
|
|
46
118
|
* Parse a value (string, number, boolean, null)
|
|
47
119
|
*/
|
|
@@ -50,15 +122,108 @@ export declare class TqlParser {
|
|
|
50
122
|
* Parse a list of values for 'in' operator
|
|
51
123
|
*/
|
|
52
124
|
private parseValueList;
|
|
125
|
+
/**
|
|
126
|
+
* Parse the optional argument list of a mutator call.
|
|
127
|
+
*
|
|
128
|
+
* `field | split(delimiter=",") eq "bob"` is valid TQL that both engines accept
|
|
129
|
+
* (`mutator = { "|" ~ !^"stats" ~ mutator_name ~ ("(" ~ mutator_args? ~ ")")? }` in
|
|
130
|
+
* tql/src/parser/grammar.pest; `mutator_with_params` in
|
|
131
|
+
* src/tql/parser_components/grammar.py). The JS parser used to stop at the mutator
|
|
132
|
+
* NAME, so the '(' fell through to the top-level "Unexpected token" arm and the editor
|
|
133
|
+
* red-squiggled a query the backend runs happily -- while this package's own completion
|
|
134
|
+
* documentation advertised that exact call form.
|
|
135
|
+
*
|
|
136
|
+
* The name token has already been consumed by the caller.
|
|
137
|
+
*/
|
|
138
|
+
private parseMutatorCall;
|
|
139
|
+
/**
|
|
140
|
+
* Parse one mutator argument: `value`, or `name=value`.
|
|
141
|
+
*
|
|
142
|
+
* Returns null (after recording an error) when the current token cannot start an
|
|
143
|
+
* argument, so the caller stops rather than spinning.
|
|
144
|
+
*/
|
|
145
|
+
private parseMutatorArgument;
|
|
146
|
+
/**
|
|
147
|
+
* Parse the value half of a mutator argument.
|
|
148
|
+
*
|
|
149
|
+
* Neither engine's grammar admits a list literal here, so `[...]` is an error rather
|
|
150
|
+
* than a silently-accepted superset.
|
|
151
|
+
*/
|
|
152
|
+
private parseMutatorArgumentValue;
|
|
53
153
|
private createFieldReference;
|
|
154
|
+
/**
|
|
155
|
+
* Consume a `not` / `!` sitting between a field and its operator, if one is there.
|
|
156
|
+
*
|
|
157
|
+
* The two-token spelling is the PRIMARY form in both grammars, not an alias: fourteen
|
|
158
|
+
* rules in `tql/src/parser/grammar.pest` are declared as
|
|
159
|
+
* `not_X_op = { neg_prefix ~ X_op | not_X_word }`, and Python builds the same shape with
|
|
160
|
+
* `(CaselessKeyword("not") | "!") + CaselessKeyword("X")`. `field not contains "x"` is
|
|
161
|
+
* how the documentation and the UI examples spell it.
|
|
162
|
+
*
|
|
163
|
+
* This package accepted only the underscore form (`not_contains`) and the glued symbol
|
|
164
|
+
* (`!contains`). Every two-token negation -- `not exists`, `not in`, `not between`,
|
|
165
|
+
* `not startswith`, `not cidr`, ten more -- was reported as
|
|
166
|
+
* `Expected operator after field 'f'`, a false red on the most readable spelling of a
|
|
167
|
+
* third of the operator surface.
|
|
168
|
+
*
|
|
169
|
+
* Only consumed when a comparison operator actually follows. Without that lookahead this
|
|
170
|
+
* would swallow the `not` in `a eq 1 and b not (c eq 2)`, where it belongs to the unary
|
|
171
|
+
* arm; leaving it in place lets the existing error recovery report on it as before.
|
|
172
|
+
*/
|
|
173
|
+
private consumeNegationPrefix;
|
|
174
|
+
/**
|
|
175
|
+
* The negated form of an operator, or null when it has none.
|
|
176
|
+
*
|
|
177
|
+
* DERIVED from `COMPARISON_OPERATORS`, not transcribed: `not X` is a TQL operator exactly
|
|
178
|
+
* when `not_X` is. That catalog is itself derived from both engine grammars and pinned by
|
|
179
|
+
* `tests/catalogParity.spec.ts`, so this rule cannot drift from the grammars without that
|
|
180
|
+
* test going red first -- and `tests/negatedOperatorForms.spec.ts` re-derives the
|
|
181
|
+
* `neg_prefix ~ X_op` rules straight out of `grammar.pest` and asserts this parser accepts
|
|
182
|
+
* exactly them.
|
|
183
|
+
*
|
|
184
|
+
* Returning null rather than fabricating `not_eq` matters. Neither engine has a
|
|
185
|
+
* `neg_prefix ~ eq_op` rule, so `f not eq 1` is a syntax error in both, and inventing an
|
|
186
|
+
* operator no evaluator dispatches on would turn a parse error into an unknown-operator
|
|
187
|
+
* failure at execution -- later, and further from the user.
|
|
188
|
+
*/
|
|
189
|
+
private negateOperator;
|
|
54
190
|
private normalizeOperator;
|
|
55
191
|
private isAtEnd;
|
|
56
192
|
private peek;
|
|
193
|
+
/** Lookahead by `offset` tokens without consuming; undefined past the end. */
|
|
194
|
+
private peekAt;
|
|
57
195
|
private previous;
|
|
58
196
|
private advance;
|
|
59
197
|
private check;
|
|
198
|
+
/**
|
|
199
|
+
* True when the current token names a mutator in `MUTATORS` but was lexed as something
|
|
200
|
+
* else because an earlier catalog claimed the name first.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately consults the catalog rather than listing the seven affected names: a list
|
|
203
|
+
* would be an eighth transcription of a set this package already maintains, and it would
|
|
204
|
+
* go stale the moment a mutator is added that collides with an aggregation.
|
|
205
|
+
*/
|
|
206
|
+
private isCatalogMutatorToken;
|
|
60
207
|
private match;
|
|
61
208
|
private matchLogical;
|
|
209
|
+
/**
|
|
210
|
+
* The canonical word a `logicalOp` token spells: `AND`, `OR` or `NOT`.
|
|
211
|
+
*
|
|
212
|
+
* `&&`, `||` and `!` are the symbolic halves of the same three grammar rules as `and`,
|
|
213
|
+
* `or` and `not` (see SYMBOL_LOGICAL_OPERATORS), so every consumer must see one name.
|
|
214
|
+
* Normalising HERE rather than in the tokenizer keeps the token's `value` equal to the
|
|
215
|
+
* source text, which the highlighting and completion surfaces read.
|
|
216
|
+
*/
|
|
217
|
+
private logicalWord;
|
|
218
|
+
/**
|
|
219
|
+
* Match the WORD spelling of a logical operator only, never the symbol.
|
|
220
|
+
*
|
|
221
|
+
* Used for the `and` inside `field between low AND high`. Both engines spell that one
|
|
222
|
+
* literally -- `value ~ ^"and" ~ value` in `grammar.pest`, `CaselessKeyword("and")` in
|
|
223
|
+
* `between_comparison_natural` -- and neither accepts `between 1 && 5`. Accepting it here
|
|
224
|
+
* would make this package the only one of the three that does.
|
|
225
|
+
*/
|
|
226
|
+
private matchLogicalWord;
|
|
62
227
|
private addError;
|
|
63
228
|
private addWarning;
|
|
64
229
|
/**
|
|
@@ -71,6 +236,33 @@ export declare class TqlParser {
|
|
|
71
236
|
* This allows the parser to continue and find additional errors.
|
|
72
237
|
*/
|
|
73
238
|
private createPlaceholderExpression;
|
|
239
|
+
/**
|
|
240
|
+
* Flag the malformed `top N` / `bottom N` spellings on a stats aggregation.
|
|
241
|
+
*
|
|
242
|
+
* ## Why this is a scan and not a stats parser
|
|
243
|
+
*
|
|
244
|
+
* This package deliberately does not build a stats AST — the engines do. But "we do not
|
|
245
|
+
* parse it" had silently become "we accept anything after `stats`": measured at HEAD, every
|
|
246
|
+
* one of `sum(x, top 10) top 5`, `sum(x) top by y` and `count(*, top 2)` is a parse error in
|
|
247
|
+
* BOTH engines and was reported valid here.
|
|
248
|
+
*
|
|
249
|
+
* A full stats parser would have to take a side on five spellings the engines currently
|
|
250
|
+
* DISAGREE about (`count(*) top 2`, `sum(x|trim) top 3`, `sum(x::number) top 3`,
|
|
251
|
+
* `sum(x|trim, top 3)`, `by y|trim top 3` — Rust accepts all five, Python refuses all five).
|
|
252
|
+
* Resolving those is an engine question. So this fires ONLY on shapes both engines refuse,
|
|
253
|
+
* and is silent everywhere else — it can never squiggle a query an engine accepts, which is
|
|
254
|
+
* the failure mode this parser keeps being repaired for.
|
|
255
|
+
*
|
|
256
|
+
* ## Why the group-by region is excluded
|
|
257
|
+
*
|
|
258
|
+
* `top` and `bottom` are ordinary identifiers, and `| stats sum(x) by top` is valid in BOTH
|
|
259
|
+
* engines — a group-by on a field named `top`. So "a `top` not followed by a number" is only
|
|
260
|
+
* unambiguous in the two AGGREGATION modifier positions: directly after the aggregation's
|
|
261
|
+
* `)`, or after a comma at the aggregation's own paren depth. `by y top` (no count) is
|
|
262
|
+
* refused by both engines and is deliberately NOT flagged here, because distinguishing it
|
|
263
|
+
* from `by top` needs the grammar this method is declining to reimplement.
|
|
264
|
+
*/
|
|
265
|
+
private checkStatsModifiers;
|
|
74
266
|
/**
|
|
75
267
|
* Check if the token stream represents a stats query.
|
|
76
268
|
* Handles: "| stats ...", "stats ...", or starts with a filter before "| stats".
|