@cloudcare/codemirror-dql 0.0.55 → 0.0.57
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/cjs/lint/hybrid.js +19 -3
- package/dist/cjs/lint/hybrid.js.map +1 -1
- package/dist/cjs/parser/parser.d.ts +104 -2
- package/dist/cjs/parser/parser.js +3083 -25
- package/dist/cjs/parser/parser.js.map +1 -1
- package/dist/esm/lint/hybrid.js +19 -3
- package/dist/esm/lint/hybrid.js.map +1 -1
- package/dist/esm/parser/parser.d.ts +104 -2
- package/dist/esm/parser/parser.js +2728 -26
- package/dist/esm/parser/parser.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,46 +1,2748 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Tree } from '@lezer/common';
|
|
2
|
+
import { ensureSyntaxTree, syntaxTreeAvailable } from '@codemirror/language';
|
|
3
|
+
const maxRepairDepth = 8;
|
|
4
|
+
const maxMismatchRepairDepth = 3;
|
|
5
|
+
const analysisBudgetMs = 100;
|
|
6
|
+
const maxDiagnostics = 100;
|
|
7
|
+
const numericClauses = new Map([
|
|
8
|
+
['LimitClause', 'LIMIT'],
|
|
9
|
+
['OffsetClause', 'OFFSET'],
|
|
10
|
+
['SLimitClause', 'SLIMIT'],
|
|
11
|
+
['SOffsetClause', 'SOFFSET'],
|
|
12
|
+
]);
|
|
13
|
+
const queryClauseLabels = new Map([
|
|
14
|
+
['TargetClause', 'projection'],
|
|
15
|
+
['FilterClause', 'WHERE'],
|
|
16
|
+
['TimeClause', 'time range'],
|
|
17
|
+
['ByClause', 'BY'],
|
|
18
|
+
['HavingClause', 'HAVING'],
|
|
19
|
+
['OrderClause', 'ORDER BY'],
|
|
20
|
+
['LimitClause', 'LIMIT'],
|
|
21
|
+
['OffsetClause', 'OFFSET'],
|
|
22
|
+
['SOrderClause', 'SORDER BY'],
|
|
23
|
+
['SLimitClause', 'SLIMIT'],
|
|
24
|
+
['SOffsetClause', 'SOFFSET'],
|
|
25
|
+
]);
|
|
26
|
+
const expressionParents = new Set(['BinaryExpr', 'Expr', 'HavingExpr', 'ProjectionExpr']);
|
|
27
|
+
const commaContextBoundaries = new Set(['CaseExpr', 'Query']);
|
|
28
|
+
export const dqlSyntaxContexts = {
|
|
29
|
+
DQL: 'DQL query',
|
|
30
|
+
DQLConditions: 'DQL condition',
|
|
31
|
+
QueryExpr: 'DQL query',
|
|
32
|
+
ShowExpr: 'SHOW expression',
|
|
33
|
+
QueryFragment: 'query fragment',
|
|
34
|
+
QueryFragmentClause: 'query fragment clause',
|
|
35
|
+
Query: 'query structure',
|
|
36
|
+
QueryClause: 'query clause',
|
|
37
|
+
Namespace: 'namespace',
|
|
38
|
+
IndexExpr: 'namespace index list',
|
|
39
|
+
IndexItem: 'namespace index',
|
|
40
|
+
DataSourceListFragment: 'data source list fragment',
|
|
41
|
+
DataSourceItemFragment: 'data source fragment',
|
|
42
|
+
DataSourceList: 'data source list',
|
|
43
|
+
DataSourceItem: 'data source',
|
|
44
|
+
QuerySource: 'nested query',
|
|
45
|
+
OuterFunctionCall: 'outer function call',
|
|
46
|
+
RawOuterFunctionCallBody: 'outer function arguments',
|
|
47
|
+
RequiredNumberOuterFunctionCallBody: 'outer function arguments',
|
|
48
|
+
OptionalNumberOuterFunctionCallBody: 'outer function arguments',
|
|
49
|
+
FillOuterFunctionCallBody: 'outer function arguments',
|
|
50
|
+
VariadicOuterFunctionCallBody: 'outer function arguments',
|
|
51
|
+
EvalExpr: 'eval expression',
|
|
52
|
+
FunctionCall: 'function call',
|
|
53
|
+
EvalIdentifier: 'eval function name',
|
|
54
|
+
RollupIdentifier: 'rollup function name',
|
|
55
|
+
RollupFunctionCall: 'rollup function call',
|
|
56
|
+
RollupFunctionCallBody: 'rollup function arguments',
|
|
57
|
+
RollupArg: 'rollup function argument',
|
|
58
|
+
FunctionIdentifier: 'function name',
|
|
59
|
+
OuterFunctionCallBody: 'outer function arguments',
|
|
60
|
+
OuterArgItem: 'outer function argument',
|
|
61
|
+
Queted: 'quoted query delimiter',
|
|
62
|
+
EvalCallBody: 'eval function arguments',
|
|
63
|
+
FunctionCallBody: 'function arguments',
|
|
64
|
+
ArgItem: 'function argument',
|
|
65
|
+
InValue: 'argument value list',
|
|
66
|
+
SingleExpr: 'expression value',
|
|
67
|
+
Expr: 'expression',
|
|
68
|
+
HavingClause: 'HAVING clause',
|
|
69
|
+
SingleHavingExpr: 'HAVING expression value',
|
|
70
|
+
HavingExpr: 'HAVING expression',
|
|
71
|
+
TargetClause: 'projection clause',
|
|
72
|
+
Projection: 'projection list',
|
|
73
|
+
ProjectFunc: 'projection function list',
|
|
74
|
+
ProjectFuncItem: 'projection function',
|
|
75
|
+
ProjectionFragment: 'projection fragment',
|
|
76
|
+
ProjectionItemFragment: 'projection fragment item',
|
|
77
|
+
ProjectionItem: 'projection item',
|
|
78
|
+
ProjectionFieldFragment: 'projection field fragment',
|
|
79
|
+
ProjectionField: 'projection field',
|
|
80
|
+
SingleFieldExpr: 'projection expression value',
|
|
81
|
+
ProjectionExpr: 'projection expression',
|
|
82
|
+
AsExpr: 'alias',
|
|
83
|
+
FilterClause: 'filter clause',
|
|
84
|
+
InFieldValue: 'filter value list',
|
|
85
|
+
FieldName: 'field name',
|
|
86
|
+
JsonField: 'JSON field path',
|
|
87
|
+
CaseExpr: 'CASE expression',
|
|
88
|
+
CaseOperand: 'CASE operand',
|
|
89
|
+
CaseWhenClause: 'CASE WHEN clause',
|
|
90
|
+
CaseElseClause: 'CASE ELSE clause',
|
|
91
|
+
CaseCondition: 'CASE condition',
|
|
92
|
+
CaseFilterExpr: 'CASE filter expression',
|
|
93
|
+
CasePredicate: 'CASE predicate',
|
|
94
|
+
CaseValue: 'CASE value',
|
|
95
|
+
CaseFunctionCall: 'CASE function call',
|
|
96
|
+
CaseFunctionIdentifier: 'CASE function name',
|
|
97
|
+
CaseFunctionCallBody: 'CASE function arguments',
|
|
98
|
+
FilterFragmentExpr: 'filter fragment',
|
|
99
|
+
ComparisonExpr: 'comparison expression',
|
|
100
|
+
LogicalFragmentExpr: 'logical filter expression',
|
|
101
|
+
LogicOp: 'logical operator',
|
|
102
|
+
NotInSeq: 'NOT IN operator',
|
|
103
|
+
Operator: 'comparison operator',
|
|
104
|
+
FilterSubExpr: 'filter subquery',
|
|
105
|
+
FilterExpr: 'filter expression',
|
|
106
|
+
BinaryExpr: 'binary expression',
|
|
107
|
+
TimeClause: 'time range clause',
|
|
108
|
+
TimeRange: 'time range',
|
|
109
|
+
TimeExpr: 'time range value',
|
|
110
|
+
ByClause: 'BY clause',
|
|
111
|
+
OrderClause: 'ORDER BY clause',
|
|
112
|
+
SOrderClause: 'SORDER BY clause',
|
|
113
|
+
LimitClause: 'LIMIT clause',
|
|
114
|
+
OffsetClause: 'OFFSET clause',
|
|
115
|
+
SLimitClause: 'SLIMIT clause',
|
|
116
|
+
SOffsetClause: 'SOFFSET clause',
|
|
117
|
+
ByExpr: 'grouping expression',
|
|
118
|
+
ByListExpr: 'grouping field list',
|
|
119
|
+
ByItem: 'grouping field',
|
|
120
|
+
FieldCall: 'grouping field function',
|
|
121
|
+
SingleByExpr: 'grouping expression value',
|
|
122
|
+
ByMathExpr: 'grouping expression',
|
|
123
|
+
NestedByMathExpr: 'nested grouping expression',
|
|
124
|
+
};
|
|
125
|
+
const functionCallBodyParents = new Set([
|
|
126
|
+
'FunctionCallBody',
|
|
127
|
+
'EvalCallBody',
|
|
128
|
+
'OuterFunctionCallBody',
|
|
129
|
+
'RawOuterFunctionCallBody',
|
|
130
|
+
'RequiredNumberOuterFunctionCallBody',
|
|
131
|
+
'OptionalNumberOuterFunctionCallBody',
|
|
132
|
+
'FillOuterFunctionCallBody',
|
|
133
|
+
'VariadicOuterFunctionCallBody',
|
|
134
|
+
'RollupFunctionCallBody',
|
|
135
|
+
'CaseFunctionCallBody',
|
|
136
|
+
]);
|
|
2
137
|
export class Parser {
|
|
3
|
-
constructor(state) {
|
|
4
|
-
this.tree = syntaxTree(state);
|
|
138
|
+
constructor(state, deadline = Date.now() + analysisBudgetMs) {
|
|
5
139
|
this.state = state;
|
|
140
|
+
this.deadline = deadline;
|
|
141
|
+
const completeTree = this.getCompleteTree(state);
|
|
142
|
+
this.tree = completeTree !== null && completeTree !== void 0 ? completeTree : Tree.empty;
|
|
143
|
+
this.treeComplete = Boolean(completeTree && completeTree.topNode.to >= state.doc.length);
|
|
6
144
|
this.diagnostics = [];
|
|
145
|
+
this.repairDepth = 0;
|
|
146
|
+
this.mismatchRepairDepth = 0;
|
|
147
|
+
this.terminalRepairUsed = false;
|
|
148
|
+
this.delimiterIssue = undefined;
|
|
149
|
+
this.quotedQueryDelimiterInfo = undefined;
|
|
150
|
+
this.caseResultDiagnostics = undefined;
|
|
151
|
+
this.caseResultRecoveryErrors = new Set();
|
|
7
152
|
}
|
|
8
153
|
getDiagnostics() {
|
|
9
|
-
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const diagnostics = [...this.diagnostics]
|
|
156
|
+
.sort((a, b) => {
|
|
10
157
|
return a.from - b.from;
|
|
158
|
+
})
|
|
159
|
+
.filter((diagnostic) => {
|
|
160
|
+
const key = `${diagnostic.from}:${diagnostic.to}:${diagnostic.message}`;
|
|
161
|
+
if (seen.has(key))
|
|
162
|
+
return false;
|
|
163
|
+
seen.add(key);
|
|
164
|
+
return true;
|
|
11
165
|
});
|
|
166
|
+
if (diagnostics.length <= maxDiagnostics)
|
|
167
|
+
return diagnostics;
|
|
168
|
+
const headCount = Math.floor((maxDiagnostics - 1) / 2);
|
|
169
|
+
const tailCount = maxDiagnostics - headCount - 1;
|
|
170
|
+
return [
|
|
171
|
+
...diagnostics.slice(0, headCount),
|
|
172
|
+
...diagnostics.slice(-tailCount),
|
|
173
|
+
{
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
source: 'DQL syntax',
|
|
176
|
+
message: `too many syntax errors; showing the first ${headCount} and last ${tailCount}`,
|
|
177
|
+
from: this.state.doc.length,
|
|
178
|
+
to: this.state.doc.length,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
12
181
|
}
|
|
13
182
|
analyze() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
183
|
+
const isPrimaryAnalysis = this.repairDepth === 0 && this.mismatchRepairDepth === 0;
|
|
184
|
+
try {
|
|
185
|
+
if (this.maskLineComments(this.state.doc.toString()).trim().length === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!this.treeComplete || Date.now() >= this.deadline) {
|
|
189
|
+
if (isPrimaryAnalysis) {
|
|
190
|
+
this.pushIncompleteAnalysisWarning();
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const delimiterIssue = this.getDelimiterIssue();
|
|
195
|
+
if (Date.now() >= this.deadline) {
|
|
196
|
+
if (isPrimaryAnalysis && this.diagnostics.length === 0)
|
|
197
|
+
this.pushIncompleteAnalysisWarning();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (isPrimaryAnalysis && !delimiterIssue)
|
|
201
|
+
this.checkAST(this.tree.topNode.firstChild);
|
|
202
|
+
if (Date.now() >= this.deadline) {
|
|
203
|
+
if (isPrimaryAnalysis && this.diagnostics.length === 0)
|
|
204
|
+
this.pushIncompleteAnalysisWarning();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.diagnoseAllErrorNodes();
|
|
208
|
+
if (isPrimaryAnalysis && Date.now() >= this.deadline && this.diagnostics.length === 0)
|
|
209
|
+
this.pushIncompleteAnalysisWarning();
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (!(error instanceof RangeError))
|
|
213
|
+
throw error;
|
|
214
|
+
this.diagnostics.length = 0;
|
|
215
|
+
if (isPrimaryAnalysis) {
|
|
216
|
+
this.diagnostics.push({
|
|
217
|
+
severity: 'warning',
|
|
218
|
+
source: 'DQL syntax',
|
|
219
|
+
message: 'syntax analysis stopped because the query is too deeply nested',
|
|
220
|
+
from: 0,
|
|
221
|
+
to: Math.min(1, this.state.doc.length),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
pushIncompleteAnalysisWarning() {
|
|
227
|
+
const message = 'syntax analysis incomplete; query is too large or deeply nested';
|
|
228
|
+
if (this.diagnostics.some((diagnostic) => diagnostic.message === message))
|
|
229
|
+
return;
|
|
230
|
+
this.diagnostics.push({
|
|
231
|
+
severity: 'warning',
|
|
232
|
+
source: 'DQL syntax',
|
|
233
|
+
message,
|
|
234
|
+
from: 0,
|
|
235
|
+
to: Math.min(1, this.state.doc.length),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
getCompleteTree(state) {
|
|
239
|
+
try {
|
|
240
|
+
if (syntaxTreeAvailable(state, state.doc.length)) {
|
|
241
|
+
const ready = ensureSyntaxTree(state, state.doc.length, 0);
|
|
242
|
+
if (ready && ready.topNode.to >= state.doc.length)
|
|
243
|
+
return ready;
|
|
244
|
+
}
|
|
245
|
+
const remaining = this.deadline - Date.now();
|
|
246
|
+
if (remaining <= 0)
|
|
247
|
+
return null;
|
|
248
|
+
const tree = ensureSyntaxTree(state, state.doc.length, Math.max(1, Math.ceil(remaining)));
|
|
249
|
+
return tree && tree.topNode.to >= state.doc.length ? tree : null;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error instanceof RangeError)
|
|
253
|
+
return null;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
18
256
|
}
|
|
19
257
|
diagnoseAllErrorNodes() {
|
|
258
|
+
var _a, _b, _c;
|
|
20
259
|
const cursor = this.tree.cursor();
|
|
260
|
+
const groups = new Set();
|
|
261
|
+
const diagnostics = [];
|
|
262
|
+
const quotedQueryInfo = this.findQuotedQueryDelimiterInfo();
|
|
263
|
+
for (const fallbackDiagnostic of quotedQueryInfo.fallbackDiagnostics) {
|
|
264
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, fallbackDiagnostic)))
|
|
265
|
+
diagnostics.push(fallbackDiagnostic);
|
|
266
|
+
}
|
|
267
|
+
if (Date.now() >= this.deadline) {
|
|
268
|
+
this.appendDiagnostics(diagnostics);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (quotedQueryInfo.fallbackRanges.length > 0) {
|
|
272
|
+
this.appendDiagnostics(diagnostics);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const delimiterIssue = this.getDelimiterIssue();
|
|
276
|
+
if ((delimiterIssue === null || delimiterIssue === void 0 ? void 0 : delimiterIssue.kind) === 'mismatched') {
|
|
277
|
+
const delimiterDiagnostic = this.makeDelimiterDiagnostic(delimiterIssue);
|
|
278
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, delimiterDiagnostic)))
|
|
279
|
+
diagnostics.push(delimiterDiagnostic);
|
|
280
|
+
if (this.mismatchRepairDepth < maxMismatchRepairDepth) {
|
|
281
|
+
const repairedDiagnostics = delimiterIssue.recovery === 'delete'
|
|
282
|
+
? this.diagnoseAfterDeletion(delimiterIssue.actualFrom, delimiterIssue.actualFrom + 1)
|
|
283
|
+
: this.diagnoseAfterReplacement(delimiterIssue.actualFrom, delimiterIssue.actualFrom + 1, delimiterIssue.close);
|
|
284
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
285
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic))) {
|
|
286
|
+
diagnostics.push(repairedDiagnostic);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const repairedDiagnostics = this.diagnoseAfterBulkDelimiterRecovery(delimiterIssue);
|
|
292
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
293
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic))) {
|
|
294
|
+
diagnostics.push(repairedDiagnostic);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
this.appendDiagnostics(diagnostics);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if ((delimiterIssue === null || delimiterIssue === void 0 ? void 0 : delimiterIssue.kind) === 'unexpectedClosing') {
|
|
302
|
+
const delimiterDiagnostic = this.makeDelimiterDiagnostic(delimiterIssue);
|
|
303
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, delimiterDiagnostic)))
|
|
304
|
+
diagnostics.push(delimiterDiagnostic);
|
|
305
|
+
const repairedDiagnostics = this.mismatchRepairDepth < maxMismatchRepairDepth
|
|
306
|
+
? this.diagnoseAfterDeletion(delimiterIssue.actualFrom, delimiterIssue.actualFrom + 1)
|
|
307
|
+
: this.diagnoseAfterBulkUnexpectedClosingRecovery(delimiterIssue);
|
|
308
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
309
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic)))
|
|
310
|
+
diagnostics.push(repairedDiagnostic);
|
|
311
|
+
}
|
|
312
|
+
this.appendDiagnostics(diagnostics);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (delimiterIssue &&
|
|
316
|
+
(delimiterIssue.kind === 'quotedQuery' ||
|
|
317
|
+
(delimiterIssue.kind === 'quote' && (!delimiterIssue.newline || Boolean((_a = delimiterIssue.repairLineBreaks) === null || _a === void 0 ? void 0 : _a.length))))) {
|
|
318
|
+
diagnostics.push(this.makeDelimiterDiagnostic(delimiterIssue));
|
|
319
|
+
if (delimiterIssue.kind === 'quote' && delimiterIssue.newline && ((_b = delimiterIssue.repairLineBreaks) === null || _b === void 0 ? void 0 : _b.length)) {
|
|
320
|
+
const repairs = this.findStringNewlineRepairs(delimiterIssue);
|
|
321
|
+
for (const repair of repairs.slice(0, maxRepairDepth + 1)) {
|
|
322
|
+
const repairDiagnostic = this.makeDiagnosticAt(repair.from, repair.from + 1, 'unescaped newline in string literal', `delimiter:${repair.from}`);
|
|
323
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairDiagnostic)))
|
|
324
|
+
diagnostics.push(repairDiagnostic);
|
|
325
|
+
}
|
|
326
|
+
const lineBreaks = [...new Set(repairs.flatMap((repair) => repair.lineBreaks))].sort((left, right) => left - right);
|
|
327
|
+
const repairedDiagnostics = this.diagnoseAfterInsertions(lineBreaks, '\\');
|
|
328
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
329
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic)))
|
|
330
|
+
diagnostics.push(repairedDiagnostic);
|
|
331
|
+
}
|
|
332
|
+
this.appendDiagnostics(diagnostics);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (this.repairDepth < maxRepairDepth &&
|
|
336
|
+
(delimiterIssue.repairAt !== undefined || (delimiterIssue.kind === 'quote' && ((_c = delimiterIssue.repairLineBreaks) === null || _c === void 0 ? void 0 : _c.length)))) {
|
|
337
|
+
const repairedDiagnostics = this.diagnoseAfterInsertion(delimiterIssue.repairAt, delimiterIssue.quote);
|
|
338
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
339
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic))) {
|
|
340
|
+
diagnostics.push(repairedDiagnostic);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this.appendDiagnostics(diagnostics);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (delimiterIssue && delimiterIssue.kind !== 'unclosed') {
|
|
348
|
+
const delimiterDiagnostic = this.makeDelimiterDiagnostic(delimiterIssue);
|
|
349
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, delimiterDiagnostic)))
|
|
350
|
+
diagnostics.push(delimiterDiagnostic);
|
|
351
|
+
if (delimiterDiagnostic.group)
|
|
352
|
+
groups.add(delimiterDiagnostic.group);
|
|
353
|
+
}
|
|
354
|
+
for (const caseDiagnostic of this.findCaseResultDiagnostics()) {
|
|
355
|
+
if (!caseDiagnostic.group || !groups.has(caseDiagnostic.group)) {
|
|
356
|
+
diagnostics.push(caseDiagnostic);
|
|
357
|
+
if (caseDiagnostic.group)
|
|
358
|
+
groups.add(caseDiagnostic.group);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
21
361
|
while (cursor.next()) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
362
|
+
if (Date.now() >= this.deadline)
|
|
363
|
+
break;
|
|
364
|
+
if (!cursor.type.isError) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (quotedQueryInfo.fallbackRanges.some((range) => cursor.from === cursor.to ? cursor.from >= range.from && cursor.from <= range.to : cursor.from < range.to && cursor.to > range.from)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const diagnostic = this.describeError(cursor.node);
|
|
371
|
+
if (!diagnostic || (diagnostic.group && groups.has(diagnostic.group))) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (diagnostics.some((existing) => existing.from === diagnostic.from && existing.to === diagnostic.to)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (diagnostic.group) {
|
|
378
|
+
groups.add(diagnostic.group);
|
|
379
|
+
}
|
|
380
|
+
diagnostics.push(diagnostic);
|
|
381
|
+
if (diagnostic.repair) {
|
|
382
|
+
if (this.repairDepth < maxRepairDepth) {
|
|
383
|
+
const repairedDiagnostics = this.diagnoseAfterInsertion(diagnostic.repair.insertAt, diagnostic.repair.text);
|
|
384
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
385
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic))) {
|
|
386
|
+
diagnostics.push(repairedDiagnostic);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (!this.terminalRepairUsed) {
|
|
391
|
+
const repairedDiagnostics = this.diagnoseAfterTerminalInsertion(diagnostic);
|
|
392
|
+
for (const repairedDiagnostic of repairedDiagnostics) {
|
|
393
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, repairedDiagnostic))) {
|
|
394
|
+
diagnostics.push(repairedDiagnostic);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
33
399
|
}
|
|
34
400
|
}
|
|
401
|
+
this.appendDiagnostics(diagnostics);
|
|
35
402
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
403
|
+
describeError(node) {
|
|
404
|
+
var _a;
|
|
405
|
+
const delimiterIssue = this.getDelimiterIssue();
|
|
406
|
+
if (delimiterIssue && delimiterIssue.kind !== 'unclosed' && this.isRelatedDelimiterError(node, delimiterIssue)) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const parent = node.parent;
|
|
410
|
+
if (!parent) {
|
|
411
|
+
return this.makeDiagnostic(node, 'incomplete DQL query');
|
|
412
|
+
}
|
|
413
|
+
const parentName = parent.name;
|
|
414
|
+
const previous = this.siblingSkippingLineComments(node, 'previous');
|
|
415
|
+
const next = this.siblingSkippingLineComments(node, 'next');
|
|
416
|
+
const previousText = previous ? this.nodeText(previous) : '';
|
|
417
|
+
const errorText = this.nodeText(node);
|
|
418
|
+
const group = `${parentName}:${parent.from}:${parent.to}:${node.from}:${node.to}`;
|
|
419
|
+
const unknownFunctionDiagnostic = this.describeUnknownFunction(node);
|
|
420
|
+
if (unknownFunctionDiagnostic)
|
|
421
|
+
return unknownFunctionDiagnostic;
|
|
422
|
+
const commaRunDiagnostic = this.describeConsecutiveCommaError(node, parent);
|
|
423
|
+
if (commaRunDiagnostic)
|
|
424
|
+
return commaRunDiagnostic;
|
|
425
|
+
if (parentName === 'Namespace') {
|
|
426
|
+
if (errorText === ':') {
|
|
427
|
+
return this.makeDiagnosticRepairing(node, 'expected "::" after namespace', node.to, ':', `Namespace:${parent.from}:${parent.to}`);
|
|
428
|
+
}
|
|
429
|
+
return this.makeDiagnostic(node, 'expected "::" after namespace', `Namespace:${parent.from}:${parent.to}`);
|
|
430
|
+
}
|
|
431
|
+
if (parentName === 'Query' && node.from === node.to && (previous === null || previous === void 0 ? void 0 : previous.name) === 'Namespace' && !this.containsError(previous)) {
|
|
432
|
+
const hasSingleColon = this.state.sliceDoc(node.from, node.from + 1) === ':';
|
|
433
|
+
return this.makeDiagnosticRepairingAt(node.from, node.from + (hasSingleColon ? 1 : 0), 'expected "::" after namespace', node.from + (hasSingleColon ? 1 : 0), hasSingleColon ? ':' : '::', `Namespace:${previous.from}:${previous.to}`);
|
|
434
|
+
}
|
|
435
|
+
if (parentName === 'Query' && node.from === node.to && (previous === null || previous === void 0 ? void 0 : previous.name) === 'DColon') {
|
|
436
|
+
return this.makeDiagnostic(node, 'data source expected after "::"', group);
|
|
437
|
+
}
|
|
438
|
+
if (parentName === 'DataSourceItem') {
|
|
439
|
+
return this.makeDiagnostic(node, 'data source expected after "::"', group);
|
|
440
|
+
}
|
|
441
|
+
if (parentName === 'DataSourceList') {
|
|
442
|
+
if (node.from === node.to && (previous === null || previous === void 0 ? void 0 : previous.name) === 'DataSourceItem' && (next === null || next === void 0 ? void 0 : next.name) === 'DataSourceItem') {
|
|
443
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
444
|
+
return this.makeDiagnostic(node, 'data source expected after ","', group);
|
|
445
|
+
}
|
|
446
|
+
if (this.looksLikeProjectionAfterMissingColon(next)) {
|
|
447
|
+
return this.makeDiagnosticRepairing(node, 'expected ":" after data source', node.from, ':', group);
|
|
448
|
+
}
|
|
449
|
+
return this.makeDiagnostic(node, 'expected "," between data sources', group);
|
|
450
|
+
}
|
|
451
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
452
|
+
return this.makeDiagnostic(node, 'data source expected after ","', group);
|
|
453
|
+
}
|
|
454
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts.DataSourceList, group);
|
|
455
|
+
}
|
|
456
|
+
if (parentName === 'TargetClause') {
|
|
457
|
+
if (node.from === node.to && (previous === null || previous === void 0 ? void 0 : previous.name) === 'Comma') {
|
|
458
|
+
return this.makeDiagnostic(node, 'projection expected after ":"', group);
|
|
459
|
+
}
|
|
460
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts.TargetClause, group);
|
|
461
|
+
}
|
|
462
|
+
if (parentName === 'Projection') {
|
|
463
|
+
if (this.isCommaSequence(errorText) || this.isCommaOnlyList(parent)) {
|
|
464
|
+
return this.makeDiagnostic(node, 'projection item expected between commas', this.commaSequenceGroup(parent));
|
|
465
|
+
}
|
|
466
|
+
if (errorText === ',' && !previous) {
|
|
467
|
+
return this.makeDiagnostic(node, 'projection item expected before ","', group);
|
|
468
|
+
}
|
|
469
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
470
|
+
return this.makeDiagnostic(node, 'projection item expected after ","', group);
|
|
471
|
+
}
|
|
472
|
+
if (node.from === node.to && (previous === null || previous === void 0 ? void 0 : previous.name) === 'ProjectionItem' && (next === null || next === void 0 ? void 0 : next.name) === 'ProjectionItem') {
|
|
473
|
+
return this.makeDiagnostic(node, 'expected "," between projection items', group);
|
|
474
|
+
}
|
|
475
|
+
return this.makeUnexpectedTokenDiagnostic(node, 'projection', group);
|
|
476
|
+
}
|
|
477
|
+
if (parentName === 'ProjectionField' && (previous === null || previous === void 0 ? void 0 : previous.name) === 'As') {
|
|
478
|
+
return this.makeDiagnostic(node, 'alias expected after "AS"', group);
|
|
479
|
+
}
|
|
480
|
+
if (parentName === 'ProjectionField' && node.from === node.to && previous && (next === null || next === void 0 ? void 0 : next.name) === 'AsExpr') {
|
|
481
|
+
if (this.maskLineComments(this.state.sliceDoc(next.to)).trimStart().startsWith('(')) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return this.makeDiagnostic(node, 'expected "AS" before projection alias', group);
|
|
485
|
+
}
|
|
486
|
+
if (parentName === 'OuterFunctionCall') {
|
|
487
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts.OuterFunctionCall, group);
|
|
488
|
+
}
|
|
489
|
+
if (functionCallBodyParents.has(parentName)) {
|
|
490
|
+
if (parentName === 'OuterFunctionCallBody' && this.hasErroredSibling(parent)) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (this.isCommaSequence(errorText) || this.isCommaOnlyList(parent)) {
|
|
494
|
+
return this.makeDiagnostic(node, 'function argument expected between commas', this.commaSequenceGroup(parent));
|
|
495
|
+
}
|
|
496
|
+
if (errorText === ',' && !previous) {
|
|
497
|
+
return this.makeDiagnostic(node, 'function argument expected before ","', group);
|
|
498
|
+
}
|
|
499
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
500
|
+
return this.makeDiagnostic(node, 'function argument expected after ","', group);
|
|
501
|
+
}
|
|
502
|
+
if (node.from === this.tree.topNode.to) {
|
|
503
|
+
const issue = this.getDelimiterIssue();
|
|
504
|
+
if ((issue === null || issue === void 0 ? void 0 : issue.kind) === 'unclosed') {
|
|
505
|
+
return this.makeDelimiterDiagnostic(issue);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return this.makeUnexpectedTokenDiagnostic(node, 'function call', group);
|
|
509
|
+
}
|
|
510
|
+
if (parentName === 'ArgItem' && node.from === node.to) {
|
|
511
|
+
if ((previous === null || previous === void 0 ? void 0 : previous.name) === 'EqlSingle') {
|
|
512
|
+
return this.makeDiagnostic(node, 'function argument value expected after "="', group);
|
|
513
|
+
}
|
|
514
|
+
return this.makeDiagnostic(node, 'expected "," between function arguments', group);
|
|
515
|
+
}
|
|
516
|
+
if (parentName === 'InValue' || parentName === 'InFieldValue') {
|
|
517
|
+
const context = parentName === 'InValue' ? 'argument value' : 'filter value';
|
|
518
|
+
if (this.isCommaSequence(errorText) || this.isCommaOnlyList(parent)) {
|
|
519
|
+
return this.makeDiagnostic(node, `${context} expected between commas`, this.commaSequenceGroup(parent));
|
|
520
|
+
}
|
|
521
|
+
if (errorText === ',' && !previous) {
|
|
522
|
+
return this.makeDiagnostic(node, `${context} expected before ","`, group);
|
|
523
|
+
}
|
|
524
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
525
|
+
return this.makeDiagnostic(node, `${context} expected after ","`, group);
|
|
526
|
+
}
|
|
527
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts[parentName], group);
|
|
528
|
+
}
|
|
529
|
+
if (parentName === 'IndexExpr' || parentName === 'IndexItem') {
|
|
530
|
+
if (this.isCommaSequence(errorText)) {
|
|
531
|
+
return this.makeDiagnostic(node, 'namespace index expected between commas', group);
|
|
532
|
+
}
|
|
533
|
+
if (errorText === ',' && !previous) {
|
|
534
|
+
return this.makeDiagnostic(node, 'namespace index expected before ","', group);
|
|
535
|
+
}
|
|
536
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
537
|
+
return this.makeDiagnostic(node, 'namespace index expected after ","', group);
|
|
538
|
+
}
|
|
539
|
+
return this.makeUnexpectedTokenDiagnostic(node, 'namespace index list', group);
|
|
540
|
+
}
|
|
541
|
+
if (parentName === 'ByListExpr') {
|
|
542
|
+
if (this.isCommaSequence(errorText)) {
|
|
543
|
+
return this.makeDiagnostic(node, 'grouping field expected between commas', group);
|
|
544
|
+
}
|
|
545
|
+
if (errorText === ',' && !previous) {
|
|
546
|
+
return this.makeDiagnostic(node, 'grouping field expected before ","', group);
|
|
547
|
+
}
|
|
548
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
549
|
+
return this.makeDiagnostic(node, 'grouping field expected after ","', group);
|
|
550
|
+
}
|
|
551
|
+
return this.makeUnexpectedTokenDiagnostic(node, 'grouping field list', group);
|
|
552
|
+
}
|
|
553
|
+
if (parentName === 'ByExpr' && errorText === ',') {
|
|
554
|
+
return this.makeDiagnostic(node, 'grouping field expected before ","', group);
|
|
555
|
+
}
|
|
556
|
+
if (expressionParents.has(parentName)) {
|
|
557
|
+
if (previous && this.isOperator(previous)) {
|
|
558
|
+
return this.makeDiagnostic(node, `right-hand expression expected after "${previousText}"`, group);
|
|
559
|
+
}
|
|
560
|
+
return this.makeUnexpectedTokenDiagnostic(node, 'expression', group);
|
|
561
|
+
}
|
|
562
|
+
const caseWhenClause = this.findAncestor(parent, 'CaseWhenClause');
|
|
563
|
+
if (caseWhenClause) {
|
|
564
|
+
const missingCondition = /^\s*when\s+(then)\b/i.exec(this.maskLineComments(this.nodeText(caseWhenClause)));
|
|
565
|
+
if (missingCondition) {
|
|
566
|
+
const thenOffset = missingCondition.index + missingCondition[0].toLowerCase().lastIndexOf('then');
|
|
567
|
+
return this.makeDiagnosticAt(caseWhenClause.from + thenOffset, caseWhenClause.from + thenOffset + 4, 'CASE condition expected after "WHEN"', this.caseGroup(caseWhenClause));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (parentName === 'CaseCondition') {
|
|
571
|
+
return this.makeDiagnostic(node, 'expected "THEN" after CASE condition', this.caseGroup(parent));
|
|
572
|
+
}
|
|
573
|
+
if (parentName === 'CaseWhenClause') {
|
|
574
|
+
if ((previous === null || previous === void 0 ? void 0 : previous.name) === 'Then') {
|
|
575
|
+
return this.makeDiagnostic(node, 'CASE result expected after "THEN"', this.caseGroup(parent));
|
|
576
|
+
}
|
|
577
|
+
if ((previous === null || previous === void 0 ? void 0 : previous.name) === 'CaseCondition' && this.containsError(previous)) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return this.makeDiagnostic(node, 'expected "THEN" after CASE condition', this.caseGroup(parent));
|
|
581
|
+
}
|
|
582
|
+
if (parentName === 'CaseElseClause') {
|
|
583
|
+
return this.makeDiagnostic(node, 'CASE result expected after "ELSE"', this.caseGroup(parent));
|
|
584
|
+
}
|
|
585
|
+
if (parentName === 'CaseExpr') {
|
|
586
|
+
if (this.caseResultRecoveryErrors.has(this.nodeRangeKey(node)))
|
|
587
|
+
return null;
|
|
588
|
+
if (errorText)
|
|
589
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts.CaseExpr, group);
|
|
590
|
+
if (previous && this.containsError(previous)) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if ((previous === null || previous === void 0 ? void 0 : previous.name) === 'CaseOperand') {
|
|
594
|
+
return this.makeDiagnostic(node, 'CASE expression requires at least one "WHEN" clause', this.caseGroup(parent));
|
|
595
|
+
}
|
|
596
|
+
return this.makeDiagnostic(node, 'expected "END" to close CASE expression', this.caseGroup(parent));
|
|
597
|
+
}
|
|
598
|
+
const numericKeyword = numericClauses.get(parentName);
|
|
599
|
+
if (numericKeyword) {
|
|
600
|
+
return this.makeDiagnostic(node, `number expected after "${numericKeyword}"`, group);
|
|
601
|
+
}
|
|
602
|
+
if (parentName === 'ByClause') {
|
|
603
|
+
return this.makeDiagnostic(node, 'grouping field expected after "BY"', group);
|
|
604
|
+
}
|
|
605
|
+
if (parentName === 'HavingClause') {
|
|
606
|
+
return this.makeDiagnostic(node, 'expression expected after "HAVING"', group);
|
|
607
|
+
}
|
|
608
|
+
if (parentName === 'OrderClause' || parentName === 'SOrderClause') {
|
|
609
|
+
const keyword = parentName === 'OrderClause' ? 'ORDER' : 'SORDER';
|
|
610
|
+
if (!parent.getChild('By')) {
|
|
611
|
+
return this.makeDiagnostic(node, `expected "BY" after "${keyword}"`, group);
|
|
612
|
+
}
|
|
613
|
+
return this.makeDiagnostic(node, `sort field expected after "${keyword} BY"`, group);
|
|
614
|
+
}
|
|
615
|
+
if (parentName === 'RollupIdentifier') {
|
|
616
|
+
const timeClause = this.findAncestor(parent, 'TimeClause');
|
|
617
|
+
let child = (_a = timeClause === null || timeClause === void 0 ? void 0 : timeClause.firstChild) !== null && _a !== void 0 ? _a : null;
|
|
618
|
+
while (child) {
|
|
619
|
+
if (child.type.isError)
|
|
620
|
+
return null;
|
|
621
|
+
child = child.nextSibling;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (parentName === 'TimeClause') {
|
|
625
|
+
if (!this.nodeText(parent).trimEnd().endsWith(']')) {
|
|
626
|
+
return this.makeDiagnostic(node, 'expected "]" to close time range', group);
|
|
627
|
+
}
|
|
628
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts.TimeClause, group);
|
|
629
|
+
}
|
|
630
|
+
const timeClause = this.findAncestor(parent, 'TimeClause');
|
|
631
|
+
if (timeClause && /^\[\s*]$/.test(this.maskLineComments(this.nodeText(timeClause)))) {
|
|
632
|
+
return this.makeDiagnostic(node, 'time range value expected between "[" and "]"', `TimeClause:${timeClause.from}:${timeClause.to}`);
|
|
633
|
+
}
|
|
634
|
+
if (parentName === 'FilterClause') {
|
|
635
|
+
if (this.isCommaSequence(errorText) || this.isCommaOnlyList(parent)) {
|
|
636
|
+
return this.makeDiagnostic(node, 'filter expression expected between commas', this.commaSequenceGroup(parent));
|
|
637
|
+
}
|
|
638
|
+
if (errorText === ',' && !previous) {
|
|
639
|
+
return this.makeDiagnostic(node, 'filter expression expected before ","', group);
|
|
640
|
+
}
|
|
641
|
+
if (this.isAdjacentToComma(node, parent)) {
|
|
642
|
+
return this.makeDiagnostic(node, 'filter expression expected after ","', group);
|
|
643
|
+
}
|
|
644
|
+
if (node.from === node.to && (previous === null || previous === void 0 ? void 0 : previous.name) === 'FilterExpr' && (next === null || next === void 0 ? void 0 : next.name) === 'FilterExpr') {
|
|
645
|
+
return this.makeDiagnostic(node, 'comparison operator expected between filter expressions', group);
|
|
646
|
+
}
|
|
647
|
+
if (!this.nodeText(parent).trimEnd().endsWith('}')) {
|
|
648
|
+
return this.makeDiagnostic(node, 'expected "}" to close filter clause', group);
|
|
649
|
+
}
|
|
650
|
+
return this.makeUnexpectedTokenDiagnostic(node, dqlSyntaxContexts.FilterClause, group);
|
|
651
|
+
}
|
|
652
|
+
if (node.from === this.tree.topNode.to) {
|
|
653
|
+
const issue = this.getDelimiterIssue();
|
|
654
|
+
if ((issue === null || issue === void 0 ? void 0 : issue.kind) === 'unclosed') {
|
|
655
|
+
return this.makeDelimiterDiagnostic(issue);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const context = this.findSyntaxContext(parent);
|
|
659
|
+
return this.makeUnexpectedTokenDiagnostic(node, context, group);
|
|
660
|
+
}
|
|
661
|
+
makeUnexpectedTokenDiagnostic(node, context, group) {
|
|
662
|
+
const text = this.nodeText(node);
|
|
663
|
+
const rawToken = text.length > 20 ? `${text.slice(0, 20)}...` : text;
|
|
664
|
+
const token = rawToken.replace(/"/g, '\\"');
|
|
665
|
+
return this.makeDiagnostic(node, token ? `unexpected token "${token}" in ${context}` : `incomplete ${context}`, group);
|
|
666
|
+
}
|
|
667
|
+
findSyntaxContext(node) {
|
|
668
|
+
let current = node;
|
|
669
|
+
while (current) {
|
|
670
|
+
const context = dqlSyntaxContexts[current.name];
|
|
671
|
+
if (context) {
|
|
672
|
+
return context;
|
|
673
|
+
}
|
|
674
|
+
current = current.parent;
|
|
675
|
+
}
|
|
676
|
+
return dqlSyntaxContexts.DQL;
|
|
677
|
+
}
|
|
678
|
+
makeDiagnostic(node, message, group) {
|
|
679
|
+
return this.makeDiagnosticAt(node.from, node.to, message, group);
|
|
680
|
+
}
|
|
681
|
+
makeDiagnosticRepairing(node, message, insertAt, text, group) {
|
|
682
|
+
return this.makeDiagnosticRepairingAt(node.from, node.to, message, insertAt, text, group);
|
|
683
|
+
}
|
|
684
|
+
makeDiagnosticRepairingAt(from, to, message, insertAt, text, group) {
|
|
685
|
+
return {
|
|
686
|
+
...this.makeDiagnosticAt(from, to, message, group),
|
|
687
|
+
repair: { insertAt, text },
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
diagnoseAfterInsertion(insertAt, text) {
|
|
691
|
+
const repairedState = this.state.update({ changes: { from: insertAt, insert: text }, filter: false }).state;
|
|
692
|
+
if (repairedState.doc.length !== this.state.doc.length + text.length) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
696
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
697
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth;
|
|
698
|
+
repairedParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
699
|
+
repairedParser.analyze();
|
|
700
|
+
return repairedParser.getDiagnostics().map((diagnostic) => ({
|
|
701
|
+
...diagnostic,
|
|
702
|
+
from: this.mapRepairedPosition(diagnostic.from, insertAt, text.length),
|
|
703
|
+
to: this.mapRepairedPosition(diagnostic.to, insertAt, text.length),
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
706
|
+
diagnoseAfterInsertions(insertAts, text) {
|
|
707
|
+
const positions = [...new Set(insertAts)].sort((left, right) => left - right);
|
|
708
|
+
if (positions.length === 0)
|
|
709
|
+
return [];
|
|
710
|
+
const repairedState = this.state.update({
|
|
711
|
+
changes: positions.map((from) => ({ from, insert: text })),
|
|
712
|
+
filter: false,
|
|
713
|
+
}).state;
|
|
714
|
+
if (repairedState.doc.length !== this.state.doc.length + positions.length * text.length) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
718
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
719
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth;
|
|
720
|
+
repairedParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
721
|
+
repairedParser.analyze();
|
|
722
|
+
return repairedParser.getDiagnostics().map((diagnostic) => ({
|
|
723
|
+
...diagnostic,
|
|
724
|
+
from: this.mapRepairedPositionAfterInsertions(diagnostic.from, positions, text.length),
|
|
725
|
+
to: this.mapRepairedPositionAfterInsertions(diagnostic.to, positions, text.length),
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
diagnoseAfterReplacement(from, to, text) {
|
|
729
|
+
const repairedState = this.state.update({ changes: { from, to, insert: text }, filter: false }).state;
|
|
730
|
+
if (repairedState.doc.length !== this.state.doc.length - (to - from) + text.length) {
|
|
731
|
+
return [];
|
|
732
|
+
}
|
|
733
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
734
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
735
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth + 1;
|
|
736
|
+
repairedParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
737
|
+
repairedParser.analyze();
|
|
738
|
+
return repairedParser.getDiagnostics();
|
|
739
|
+
}
|
|
740
|
+
diagnoseAfterDeletion(from, to) {
|
|
741
|
+
const repairedState = this.state.update({ changes: { from, to, insert: '' }, filter: false }).state;
|
|
742
|
+
const deletedLength = to - from;
|
|
743
|
+
if (repairedState.doc.length !== this.state.doc.length - deletedLength) {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
747
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
748
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth + 1;
|
|
749
|
+
repairedParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
750
|
+
repairedParser.analyze();
|
|
751
|
+
return repairedParser.getDiagnostics().map((diagnostic) => ({
|
|
752
|
+
...diagnostic,
|
|
753
|
+
from: diagnostic.from < from ? diagnostic.from : diagnostic.from + deletedLength,
|
|
754
|
+
to: diagnostic.to <= from && diagnostic.from < from ? diagnostic.to : diagnostic.to + deletedLength,
|
|
755
|
+
}));
|
|
756
|
+
}
|
|
757
|
+
diagnoseAfterBulkDelimiterRecovery(issue) {
|
|
758
|
+
var _a;
|
|
759
|
+
const firstTransaction = this.state.update({
|
|
760
|
+
changes: {
|
|
761
|
+
from: issue.actualFrom,
|
|
762
|
+
to: issue.actualFrom + 1,
|
|
763
|
+
insert: issue.recovery === 'delete' ? '' : issue.close,
|
|
764
|
+
},
|
|
765
|
+
filter: false,
|
|
766
|
+
});
|
|
767
|
+
const bulkChanges = this.findBulkDelimiterRecoveryChanges(firstTransaction.state);
|
|
768
|
+
if (bulkChanges === null)
|
|
769
|
+
return [];
|
|
770
|
+
const bulkTransaction = bulkChanges.length > 0 ? firstTransaction.state.update({ changes: bulkChanges, filter: false }) : null;
|
|
771
|
+
const repairedState = (_a = bulkTransaction === null || bulkTransaction === void 0 ? void 0 : bulkTransaction.state) !== null && _a !== void 0 ? _a : firstTransaction.state;
|
|
772
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
773
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
774
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth + 1;
|
|
775
|
+
repairedParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
776
|
+
repairedParser.analyze();
|
|
777
|
+
let diagnostics = repairedParser.getDiagnostics();
|
|
778
|
+
if (bulkTransaction) {
|
|
779
|
+
diagnostics = this.mapDiagnosticsThroughChanges(diagnostics, bulkTransaction.changes);
|
|
780
|
+
}
|
|
781
|
+
return this.mapDiagnosticsThroughChanges(diagnostics, firstTransaction.changes);
|
|
782
|
+
}
|
|
783
|
+
diagnoseAfterBulkUnexpectedClosingRecovery(issue) {
|
|
784
|
+
var _a;
|
|
785
|
+
const firstTransaction = this.state.update({
|
|
786
|
+
changes: { from: issue.actualFrom, to: issue.actualFrom + 1, insert: '' },
|
|
787
|
+
filter: false,
|
|
43
788
|
});
|
|
789
|
+
const bulkChanges = this.findBulkDelimiterRecoveryChanges(firstTransaction.state);
|
|
790
|
+
if (bulkChanges === null)
|
|
791
|
+
return [];
|
|
792
|
+
const bulkTransaction = bulkChanges.length > 0 ? firstTransaction.state.update({ changes: bulkChanges, filter: false }) : null;
|
|
793
|
+
const repairedState = (_a = bulkTransaction === null || bulkTransaction === void 0 ? void 0 : bulkTransaction.state) !== null && _a !== void 0 ? _a : firstTransaction.state;
|
|
794
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
795
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
796
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth + 1;
|
|
797
|
+
repairedParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
798
|
+
repairedParser.analyze();
|
|
799
|
+
let diagnostics = repairedParser.getDiagnostics();
|
|
800
|
+
if (bulkTransaction)
|
|
801
|
+
diagnostics = this.mapDiagnosticsThroughChanges(diagnostics, bulkTransaction.changes);
|
|
802
|
+
return this.mapDiagnosticsThroughChanges(diagnostics, firstTransaction.changes);
|
|
803
|
+
}
|
|
804
|
+
diagnoseAfterTerminalInsertion(diagnostic) {
|
|
805
|
+
var _a;
|
|
806
|
+
if (!diagnostic.repair)
|
|
807
|
+
return [];
|
|
808
|
+
const firstTransaction = this.state.update({
|
|
809
|
+
changes: { from: diagnostic.repair.insertAt, insert: diagnostic.repair.text },
|
|
810
|
+
filter: false,
|
|
811
|
+
});
|
|
812
|
+
const matchingRepairs = this.findMatchingRepairs(firstTransaction.state, diagnostic.message);
|
|
813
|
+
if (diagnostic.message === 'expected "::" after namespace') {
|
|
814
|
+
for (const from of this.findLikelyNamespaceSeparatorInsertions(firstTransaction.state.doc.toString())) {
|
|
815
|
+
matchingRepairs.push({ from, insert: ':' });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const uniqueRepairs = matchingRepairs
|
|
819
|
+
.sort((left, right) => left.from - right.from)
|
|
820
|
+
.filter((repair, index, repairs) => index === 0 || repair.from !== repairs[index - 1].from || repair.insert !== repairs[index - 1].insert);
|
|
821
|
+
const bulkTransaction = uniqueRepairs.length > 0
|
|
822
|
+
? firstTransaction.state.update({
|
|
823
|
+
changes: uniqueRepairs,
|
|
824
|
+
filter: false,
|
|
825
|
+
})
|
|
826
|
+
: null;
|
|
827
|
+
const repairedState = (_a = bulkTransaction === null || bulkTransaction === void 0 ? void 0 : bulkTransaction.state) !== null && _a !== void 0 ? _a : firstTransaction.state;
|
|
828
|
+
const repairedParser = new Parser(repairedState, this.deadline);
|
|
829
|
+
repairedParser.repairDepth = this.repairDepth + 1;
|
|
830
|
+
repairedParser.mismatchRepairDepth = this.mismatchRepairDepth;
|
|
831
|
+
repairedParser.terminalRepairUsed = true;
|
|
832
|
+
repairedParser.analyze();
|
|
833
|
+
let diagnostics = repairedParser.getDiagnostics();
|
|
834
|
+
if (bulkTransaction) {
|
|
835
|
+
diagnostics = this.mapDiagnosticsThroughChanges(diagnostics, bulkTransaction.changes);
|
|
836
|
+
}
|
|
837
|
+
return this.mapDiagnosticsThroughChanges(diagnostics, firstTransaction.changes);
|
|
838
|
+
}
|
|
839
|
+
findStringNewlineRepairs(issue) {
|
|
840
|
+
var _a;
|
|
841
|
+
const text = this.state.doc.toString();
|
|
842
|
+
const repairs = new Map();
|
|
843
|
+
if ((_a = issue.repairLineBreaks) === null || _a === void 0 ? void 0 : _a.length)
|
|
844
|
+
repairs.set(issue.from, issue.repairLineBreaks);
|
|
845
|
+
const cursor = this.tree.cursor();
|
|
846
|
+
while (cursor.next()) {
|
|
847
|
+
if (cursor.name !== 'StringLiteral' || cursor.from >= cursor.to || text[cursor.to] !== '\n')
|
|
848
|
+
continue;
|
|
849
|
+
if (this.findAncestor(cursor.node.parent, 'IndexItem'))
|
|
850
|
+
continue;
|
|
851
|
+
const quote = text[cursor.from];
|
|
852
|
+
if (quote !== '"' && quote !== "'")
|
|
853
|
+
continue;
|
|
854
|
+
const recovery = this.findStringNewlineRecovery(text, cursor.from + 1, quote);
|
|
855
|
+
if (recovery === null || recovery === void 0 ? void 0 : recovery.lineBreaks.length)
|
|
856
|
+
repairs.set(cursor.from, recovery.lineBreaks);
|
|
857
|
+
}
|
|
858
|
+
const transparent = this.findLexicalQuotedQueryWrapperDelimiters(this.tree, text);
|
|
859
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
860
|
+
if (transparent.has(index))
|
|
861
|
+
continue;
|
|
862
|
+
const char = text[index];
|
|
863
|
+
if (char === '#') {
|
|
864
|
+
while (index + 1 < text.length && text[index + 1] !== '\n')
|
|
865
|
+
index += 1;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (char.charCodeAt(0) === 96) {
|
|
869
|
+
const closing = text.indexOf(char, index + 1);
|
|
870
|
+
if (closing < 0)
|
|
871
|
+
break;
|
|
872
|
+
index = closing;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (char !== '"' && char !== "'")
|
|
876
|
+
continue;
|
|
877
|
+
const recovery = this.findStringNewlineRecovery(text, index + 1, char);
|
|
878
|
+
if (!recovery)
|
|
879
|
+
break;
|
|
880
|
+
if (recovery.lineBreaks.length)
|
|
881
|
+
repairs.set(index, recovery.lineBreaks);
|
|
882
|
+
index = recovery.end - 1;
|
|
883
|
+
}
|
|
884
|
+
return [...repairs.entries()].map(([from, lineBreaks]) => ({ from, lineBreaks })).sort((left, right) => left.from - right.from);
|
|
885
|
+
}
|
|
886
|
+
findLexicalQuotedQueryWrapperDelimiters(tree, text) {
|
|
887
|
+
const transparent = new Set();
|
|
888
|
+
const cursor = tree.cursor();
|
|
889
|
+
while (cursor.next()) {
|
|
890
|
+
if (cursor.name !== 'OuterArgItem' || !cursor.node.getChild('Query'))
|
|
891
|
+
continue;
|
|
892
|
+
const opening = cursor.node.getChildren('Queted')[0];
|
|
893
|
+
if (!opening)
|
|
894
|
+
continue;
|
|
895
|
+
const quote = text[opening.from];
|
|
896
|
+
const closing = this.findLexicalQuotedQueryClosing(text, opening.from, quote);
|
|
897
|
+
if (closing !== null) {
|
|
898
|
+
transparent.add(opening.from);
|
|
899
|
+
transparent.add(closing);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return transparent;
|
|
903
|
+
}
|
|
904
|
+
findLexicalQuotedQueryClosing(text, opening, wrapperQuote) {
|
|
905
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
906
|
+
const closers = new Set(Object.values(pairs));
|
|
907
|
+
const stack = [];
|
|
908
|
+
let quote = '';
|
|
909
|
+
let escaped = false;
|
|
910
|
+
let inComment = false;
|
|
911
|
+
for (let index = opening + 1; index < text.length; index += 1) {
|
|
912
|
+
if (Date.now() >= this.deadline)
|
|
913
|
+
return null;
|
|
914
|
+
const char = text[index];
|
|
915
|
+
if (inComment) {
|
|
916
|
+
if (char === '\n')
|
|
917
|
+
inComment = false;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (quote) {
|
|
921
|
+
if (escaped) {
|
|
922
|
+
escaped = false;
|
|
923
|
+
}
|
|
924
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
925
|
+
escaped = true;
|
|
926
|
+
}
|
|
927
|
+
else if (char === quote) {
|
|
928
|
+
quote = '';
|
|
929
|
+
}
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
const following = char === wrapperQuote ? this.nextNonTriviaCharacter(text, index + 1) : '';
|
|
933
|
+
if (following === ')' || following === ',')
|
|
934
|
+
return index;
|
|
935
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
936
|
+
quote = char;
|
|
937
|
+
}
|
|
938
|
+
else if (char === '#') {
|
|
939
|
+
inComment = true;
|
|
940
|
+
}
|
|
941
|
+
else if (pairs[char]) {
|
|
942
|
+
stack.push(pairs[char]);
|
|
943
|
+
}
|
|
944
|
+
else if (closers.has(char)) {
|
|
945
|
+
if (stack[stack.length - 1] === char)
|
|
946
|
+
stack.pop();
|
|
947
|
+
else if (stack.length > 0)
|
|
948
|
+
stack.pop();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
nextNonTriviaCharacter(text, start) {
|
|
954
|
+
let inComment = false;
|
|
955
|
+
for (let index = start; index < text.length; index += 1) {
|
|
956
|
+
if (Date.now() >= this.deadline)
|
|
957
|
+
return '';
|
|
958
|
+
const char = text[index];
|
|
959
|
+
if (inComment) {
|
|
960
|
+
if (char === '\n')
|
|
961
|
+
inComment = false;
|
|
962
|
+
}
|
|
963
|
+
else if (char === '#') {
|
|
964
|
+
inComment = true;
|
|
965
|
+
}
|
|
966
|
+
else if (!/\s/.test(char)) {
|
|
967
|
+
return char;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return '';
|
|
971
|
+
}
|
|
972
|
+
findMatchingRepairs(state, message) {
|
|
973
|
+
const candidate = new Parser(state, this.deadline);
|
|
974
|
+
if (!candidate.treeComplete)
|
|
975
|
+
return [];
|
|
976
|
+
const repairs = [];
|
|
977
|
+
const cursor = candidate.tree.cursor();
|
|
978
|
+
while (cursor.next()) {
|
|
979
|
+
if (!cursor.type.isError)
|
|
980
|
+
continue;
|
|
981
|
+
const described = candidate.describeError(cursor.node);
|
|
982
|
+
if ((described === null || described === void 0 ? void 0 : described.message) === message && described.repair) {
|
|
983
|
+
repairs.push({ from: described.repair.insertAt, insert: described.repair.text });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return repairs;
|
|
987
|
+
}
|
|
988
|
+
mapDiagnosticsThroughChanges(diagnostics, changes) {
|
|
989
|
+
const inverse = changes.invertedDesc;
|
|
990
|
+
return diagnostics.map((diagnostic) => ({
|
|
991
|
+
...diagnostic,
|
|
992
|
+
from: inverse.mapPos(diagnostic.from, 1),
|
|
993
|
+
to: inverse.mapPos(diagnostic.to, diagnostic.from === diagnostic.to ? 1 : -1),
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
findLikelyNamespaceSeparatorInsertions(text) {
|
|
997
|
+
const masked = this.maskStringsAndLineComments(text);
|
|
998
|
+
const pattern = /(?:^|\()\s*[A-Za-z_-][A-Za-z0-9_-]*(?:\([^()\n]*\))?\s*:(?!:)/g;
|
|
999
|
+
const insertions = [];
|
|
1000
|
+
let match = pattern.exec(masked);
|
|
1001
|
+
while (match) {
|
|
1002
|
+
insertions.push(match.index + match[0].lastIndexOf(':') + 1);
|
|
1003
|
+
match = pattern.exec(masked);
|
|
1004
|
+
}
|
|
1005
|
+
return [...new Set(insertions)];
|
|
1006
|
+
}
|
|
1007
|
+
findBulkDelimiterRecoveryChanges(state) {
|
|
1008
|
+
var _a, _b, _c;
|
|
1009
|
+
if (Date.now() >= this.deadline)
|
|
1010
|
+
return null;
|
|
1011
|
+
const text = state.doc.toString();
|
|
1012
|
+
const candidate = new Parser(state, this.deadline);
|
|
1013
|
+
if (!candidate.treeComplete)
|
|
1014
|
+
return null;
|
|
1015
|
+
const transparent = candidate.findQuotedQueryDelimiterInfo().transparent;
|
|
1016
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
1017
|
+
const closers = new Set(Object.values(pairs));
|
|
1018
|
+
const tokens = [];
|
|
1019
|
+
let quote = '';
|
|
1020
|
+
let escaped = false;
|
|
1021
|
+
let inComment = false;
|
|
1022
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1023
|
+
if (Date.now() >= this.deadline)
|
|
1024
|
+
return null;
|
|
1025
|
+
const char = text[index];
|
|
1026
|
+
if (transparent.has(index))
|
|
1027
|
+
continue;
|
|
1028
|
+
if (inComment) {
|
|
1029
|
+
if (char === '\n')
|
|
1030
|
+
inComment = false;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (quote) {
|
|
1034
|
+
if (escaped) {
|
|
1035
|
+
escaped = false;
|
|
1036
|
+
}
|
|
1037
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
1038
|
+
escaped = true;
|
|
1039
|
+
}
|
|
1040
|
+
else if (char === quote) {
|
|
1041
|
+
quote = '';
|
|
1042
|
+
}
|
|
1043
|
+
else if (char === '\n' && quote.charCodeAt(0) !== 96) {
|
|
1044
|
+
quote = '';
|
|
1045
|
+
}
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
1049
|
+
quote = char;
|
|
1050
|
+
}
|
|
1051
|
+
else if (char === '#') {
|
|
1052
|
+
inComment = true;
|
|
1053
|
+
}
|
|
1054
|
+
else if (pairs[char] || closers.has(char)) {
|
|
1055
|
+
tokens.push({ char, from: index });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const stack = [];
|
|
1059
|
+
const stackCloserCounts = new Map();
|
|
1060
|
+
const futureCloserCounts = new Map();
|
|
1061
|
+
const changes = [];
|
|
1062
|
+
for (const token of tokens) {
|
|
1063
|
+
if (Date.now() >= this.deadline)
|
|
1064
|
+
return null;
|
|
1065
|
+
if (pairs[token.char]) {
|
|
1066
|
+
const closer = pairs[token.char];
|
|
1067
|
+
stack.push(closer);
|
|
1068
|
+
stackCloserCounts.set(closer, ((_a = stackCloserCounts.get(closer)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
1069
|
+
futureCloserCounts.clear();
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const expected = stack[stack.length - 1];
|
|
1073
|
+
if (!expected) {
|
|
1074
|
+
changes.push({ from: token.from, to: token.from + 1, insert: '' });
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (token.char === expected) {
|
|
1078
|
+
stack.pop();
|
|
1079
|
+
stackCloserCounts.set(expected, ((_b = stackCloserCounts.get(expected)) !== null && _b !== void 0 ? _b : 1) - 1);
|
|
1080
|
+
futureCloserCounts.clear();
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const sameCloserDepth = (_c = stackCloserCounts.get(expected)) !== null && _c !== void 0 ? _c : 0;
|
|
1084
|
+
let futureCloserCount = futureCloserCounts.get(expected);
|
|
1085
|
+
if (futureCloserCount === undefined) {
|
|
1086
|
+
const countedClosers = this.countFutureClosersAtCurrentDepth(text, token.from + 1, expected, transparent);
|
|
1087
|
+
if (countedClosers === null)
|
|
1088
|
+
return null;
|
|
1089
|
+
futureCloserCount = countedClosers;
|
|
1090
|
+
futureCloserCounts.set(expected, countedClosers);
|
|
1091
|
+
}
|
|
1092
|
+
if (futureCloserCount >= sameCloserDepth) {
|
|
1093
|
+
changes.push({ from: token.from, to: token.from + 1, insert: '' });
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
changes.push({ from: token.from, to: token.from + 1, insert: expected });
|
|
1097
|
+
stack.pop();
|
|
1098
|
+
stackCloserCounts.set(expected, sameCloserDepth - 1);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return changes;
|
|
1102
|
+
}
|
|
1103
|
+
isSameDiagnostic(left, right) {
|
|
1104
|
+
return left.from === right.from && left.to === right.to && left.message === right.message;
|
|
1105
|
+
}
|
|
1106
|
+
appendDiagnostics(diagnostics) {
|
|
1107
|
+
for (const diagnostic of diagnostics) {
|
|
1108
|
+
const { group: _group, repair: _repair, ...codeMirrorDiagnostic } = diagnostic;
|
|
1109
|
+
this.diagnostics.push(codeMirrorDiagnostic);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
mapRepairedPosition(position, insertAt, insertedLength) {
|
|
1113
|
+
return position <= insertAt ? position : Math.max(insertAt, position - insertedLength);
|
|
1114
|
+
}
|
|
1115
|
+
mapRepairedPositionAfterInsertions(position, insertAts, insertedLength) {
|
|
1116
|
+
let mapped = position;
|
|
1117
|
+
let priorInsertedLength = 0;
|
|
1118
|
+
for (const insertAt of insertAts) {
|
|
1119
|
+
if (position > insertAt + priorInsertedLength) {
|
|
1120
|
+
mapped -= insertedLength;
|
|
1121
|
+
}
|
|
1122
|
+
priorInsertedLength += insertedLength;
|
|
1123
|
+
}
|
|
1124
|
+
return mapped;
|
|
1125
|
+
}
|
|
1126
|
+
makeDiagnosticAt(from, to, message, group) {
|
|
1127
|
+
return {
|
|
1128
|
+
severity: 'error',
|
|
1129
|
+
source: 'DQL syntax',
|
|
1130
|
+
message,
|
|
1131
|
+
from,
|
|
1132
|
+
to,
|
|
1133
|
+
group,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
nodeText(node) {
|
|
1137
|
+
return this.state.sliceDoc(node.from, node.to);
|
|
1138
|
+
}
|
|
1139
|
+
isAdjacentToComma(node, parent) {
|
|
1140
|
+
const before = this.maskLineComments(this.state.sliceDoc(parent.from, node.from)).trimEnd();
|
|
1141
|
+
const after = this.maskLineComments(this.state.sliceDoc(node.to, parent.to)).trimStart();
|
|
1142
|
+
return before.endsWith(',') || after.startsWith(',') || this.nodeText(node) === ',';
|
|
1143
|
+
}
|
|
1144
|
+
isCommaSequence(text) {
|
|
1145
|
+
return /^,{2,}$/.test(this.maskLineComments(text).replace(/\s/g, ''));
|
|
1146
|
+
}
|
|
1147
|
+
isCommaOnlyList(node) {
|
|
1148
|
+
return /^(?:\(,{2,}\)|\[,{2,}\]|\{,{2,}\})$/.test(this.maskLineComments(this.nodeText(node)).replace(/\s/g, ''));
|
|
1149
|
+
}
|
|
1150
|
+
commaSequenceGroup(node) {
|
|
1151
|
+
return `${node.name}:${node.from}:${node.to}:comma-sequence`;
|
|
1152
|
+
}
|
|
1153
|
+
describeConsecutiveCommaError(node, parent) {
|
|
1154
|
+
const contexts = {
|
|
1155
|
+
Projection: 'projection item',
|
|
1156
|
+
FunctionCallBody: 'function argument',
|
|
1157
|
+
EvalCallBody: 'function argument',
|
|
1158
|
+
OuterFunctionCallBody: 'function argument',
|
|
1159
|
+
RawOuterFunctionCallBody: 'function argument',
|
|
1160
|
+
RequiredNumberOuterFunctionCallBody: 'function argument',
|
|
1161
|
+
OptionalNumberOuterFunctionCallBody: 'function argument',
|
|
1162
|
+
FillOuterFunctionCallBody: 'function argument',
|
|
1163
|
+
VariadicOuterFunctionCallBody: 'function argument',
|
|
1164
|
+
RollupFunctionCallBody: 'function argument',
|
|
1165
|
+
CaseFunctionCallBody: 'function argument',
|
|
1166
|
+
InValue: 'argument value',
|
|
1167
|
+
InFieldValue: 'filter value',
|
|
1168
|
+
IndexExpr: 'namespace index',
|
|
1169
|
+
ByListExpr: 'grouping field',
|
|
1170
|
+
FilterClause: 'filter expression',
|
|
1171
|
+
};
|
|
1172
|
+
let context = parent;
|
|
1173
|
+
while (context && !contexts[context.name]) {
|
|
1174
|
+
if (commaContextBoundaries.has(context.name))
|
|
1175
|
+
return null;
|
|
1176
|
+
context = context.parent;
|
|
1177
|
+
}
|
|
1178
|
+
if (!context)
|
|
1179
|
+
return null;
|
|
1180
|
+
const masked = this.maskStringsAndLineComments(this.nodeText(context));
|
|
1181
|
+
const commaRun = /(?:,\s*){2,}/g;
|
|
1182
|
+
let match = commaRun.exec(masked);
|
|
1183
|
+
while (match) {
|
|
1184
|
+
const lastCommaOffset = match.index + match[0].lastIndexOf(',');
|
|
1185
|
+
const runFrom = context.from + match.index;
|
|
1186
|
+
const runTo = context.from + lastCommaOffset + 1;
|
|
1187
|
+
const recoveredCase = context.name === 'Projection' ? this.findCaseExprEndingAt(context, runFrom) : null;
|
|
1188
|
+
if (recoveredCase && this.containsError(recoveredCase) && node.from <= runTo && node.to >= runFrom) {
|
|
1189
|
+
return this.makeDiagnosticAt(runFrom, runFrom, 'CASE result expected after "THEN"', 'CaseResult:' + recoveredCase.from + ':' + recoveredCase.to);
|
|
1190
|
+
}
|
|
1191
|
+
const overlapsRun = node.from === node.to
|
|
1192
|
+
? (node.from > runFrom || !this.hasMissingValueBeforeComma(node)) && node.from <= runTo
|
|
1193
|
+
: node.from < runTo && node.to > runFrom;
|
|
1194
|
+
if (overlapsRun) {
|
|
1195
|
+
const before = masked
|
|
1196
|
+
.slice(0, match.index)
|
|
1197
|
+
.replace(/\s/g, '')
|
|
1198
|
+
.replace(/^[([{]/, '');
|
|
1199
|
+
const message = before ? `${contexts[context.name]} expected after ","` : `${contexts[context.name]} expected between commas`;
|
|
1200
|
+
return this.makeDiagnosticAt(node.from, node.to, message, `${context.name}:${context.from}:${runFrom}:${runTo}:comma-sequence`);
|
|
1201
|
+
}
|
|
1202
|
+
match = commaRun.exec(masked);
|
|
1203
|
+
}
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
hasMissingValueBeforeComma(node) {
|
|
1207
|
+
const previous = this.siblingSkippingLineComments(node, 'previous');
|
|
1208
|
+
return Boolean(previous && (this.isOperator(previous) || previous.name === 'Then' || previous.name === 'Else'));
|
|
1209
|
+
}
|
|
1210
|
+
findCaseExprEndingAt(node, position) {
|
|
1211
|
+
let child = node.firstChild;
|
|
1212
|
+
while (child) {
|
|
1213
|
+
if (child.name === 'CaseExpr' && child.to === position)
|
|
1214
|
+
return child;
|
|
1215
|
+
const nested = this.findCaseExprEndingAt(child, position);
|
|
1216
|
+
if (nested)
|
|
1217
|
+
return nested;
|
|
1218
|
+
child = child.nextSibling;
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
siblingSkippingLineComments(node, direction) {
|
|
1223
|
+
let sibling = direction === 'previous' ? node.prevSibling : node.nextSibling;
|
|
1224
|
+
while ((sibling === null || sibling === void 0 ? void 0 : sibling.name) === 'LineComment') {
|
|
1225
|
+
sibling = direction === 'previous' ? sibling.prevSibling : sibling.nextSibling;
|
|
1226
|
+
}
|
|
1227
|
+
return sibling;
|
|
1228
|
+
}
|
|
1229
|
+
hasErroredSibling(node) {
|
|
1230
|
+
let sibling = node.prevSibling;
|
|
1231
|
+
while (sibling) {
|
|
1232
|
+
if (sibling.type.isError || this.containsError(sibling)) {
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
sibling = sibling.prevSibling;
|
|
1236
|
+
}
|
|
1237
|
+
sibling = node.nextSibling;
|
|
1238
|
+
while (sibling) {
|
|
1239
|
+
if (sibling.type.isError || this.containsError(sibling)) {
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
sibling = sibling.nextSibling;
|
|
1243
|
+
}
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
maskLineComments(text) {
|
|
1247
|
+
let masked = '';
|
|
1248
|
+
let quote = '';
|
|
1249
|
+
let escaped = false;
|
|
1250
|
+
let inComment = false;
|
|
1251
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1252
|
+
const char = text[index];
|
|
1253
|
+
if (inComment) {
|
|
1254
|
+
if (char === '\n') {
|
|
1255
|
+
inComment = false;
|
|
1256
|
+
masked += char;
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
masked += ' ';
|
|
1260
|
+
}
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (quote) {
|
|
1264
|
+
masked += char;
|
|
1265
|
+
if (escaped) {
|
|
1266
|
+
escaped = false;
|
|
1267
|
+
}
|
|
1268
|
+
else if (char === '\\' && quote !== '`') {
|
|
1269
|
+
escaped = true;
|
|
1270
|
+
}
|
|
1271
|
+
else if (char === quote) {
|
|
1272
|
+
quote = '';
|
|
1273
|
+
}
|
|
1274
|
+
else if (char === '\n' && quote !== '`') {
|
|
1275
|
+
quote = '';
|
|
1276
|
+
}
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1280
|
+
quote = char;
|
|
1281
|
+
masked += char;
|
|
1282
|
+
}
|
|
1283
|
+
else if (char === '#') {
|
|
1284
|
+
inComment = true;
|
|
1285
|
+
masked += ' ';
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
masked += char;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return masked;
|
|
1292
|
+
}
|
|
1293
|
+
maskStringsAndLineComments(text) {
|
|
1294
|
+
let masked = '';
|
|
1295
|
+
let quote = '';
|
|
1296
|
+
let escaped = false;
|
|
1297
|
+
let inComment = false;
|
|
1298
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1299
|
+
const char = text[index];
|
|
1300
|
+
if (inComment) {
|
|
1301
|
+
if (char === '\n') {
|
|
1302
|
+
inComment = false;
|
|
1303
|
+
masked += char;
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
masked += ' ';
|
|
1307
|
+
}
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (quote) {
|
|
1311
|
+
masked += char === '\n' ? char : ' ';
|
|
1312
|
+
if (escaped) {
|
|
1313
|
+
escaped = false;
|
|
1314
|
+
}
|
|
1315
|
+
else if (char === '\\' && quote !== '`') {
|
|
1316
|
+
escaped = true;
|
|
1317
|
+
}
|
|
1318
|
+
else if (char === quote) {
|
|
1319
|
+
quote = '';
|
|
1320
|
+
}
|
|
1321
|
+
else if (char === '\n' && quote !== '`') {
|
|
1322
|
+
quote = '';
|
|
1323
|
+
}
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1327
|
+
quote = char;
|
|
1328
|
+
masked += 'x';
|
|
1329
|
+
}
|
|
1330
|
+
else if (char === '#') {
|
|
1331
|
+
inComment = true;
|
|
1332
|
+
masked += ' ';
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
masked += char;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return masked;
|
|
1339
|
+
}
|
|
1340
|
+
containsError(node) {
|
|
1341
|
+
let child = node.firstChild;
|
|
1342
|
+
while (child) {
|
|
1343
|
+
if (child.type.isError || this.containsError(child)) {
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
child = child.nextSibling;
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
looksLikeProjectionAfterMissingColon(node) {
|
|
1351
|
+
const querySource = node.getChild('QuerySource');
|
|
1352
|
+
return Boolean(querySource && this.containsError(querySource) && !this.containsNode(querySource, 'DColon'));
|
|
1353
|
+
}
|
|
1354
|
+
containsNode(node, name) {
|
|
1355
|
+
let child = node.firstChild;
|
|
1356
|
+
while (child) {
|
|
1357
|
+
if (child.name === name || this.containsNode(child, name)) {
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
child = child.nextSibling;
|
|
1361
|
+
}
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
findAncestor(node, name) {
|
|
1365
|
+
let current = node;
|
|
1366
|
+
while (current) {
|
|
1367
|
+
if (current.name === name) {
|
|
1368
|
+
return current;
|
|
1369
|
+
}
|
|
1370
|
+
current = current.parent;
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
caseGroup(node) {
|
|
1375
|
+
let current = node;
|
|
1376
|
+
while (current && current.name !== 'CaseWhenClause' && current.name !== 'CaseElseClause' && current.name !== 'CaseExpr') {
|
|
1377
|
+
current = current.parent;
|
|
1378
|
+
}
|
|
1379
|
+
return current ? `${current.name}:${current.from}:${current.to}` : `Case:${node.from}:${node.to}`;
|
|
1380
|
+
}
|
|
1381
|
+
findCaseResultDiagnostics() {
|
|
1382
|
+
if (this.caseResultDiagnostics)
|
|
1383
|
+
return this.caseResultDiagnostics;
|
|
1384
|
+
const diagnostics = [];
|
|
1385
|
+
const seen = new Set();
|
|
1386
|
+
const cursor = this.tree.cursor();
|
|
1387
|
+
while (cursor.next()) {
|
|
1388
|
+
if (cursor.name !== 'CaseExpr')
|
|
1389
|
+
continue;
|
|
1390
|
+
let clause = cursor.node.firstChild;
|
|
1391
|
+
while (clause) {
|
|
1392
|
+
const isWhenClause = clause.name === 'CaseWhenClause';
|
|
1393
|
+
const isElseClause = clause.name === 'CaseElseClause';
|
|
1394
|
+
const recovery = this.siblingSkippingLineComments(clause, 'next');
|
|
1395
|
+
if ((isWhenClause || isElseClause) && (recovery === null || recovery === void 0 ? void 0 : recovery.type.isError)) {
|
|
1396
|
+
const clauseText = this.maskStringsAndLineComments(this.nodeText(clause));
|
|
1397
|
+
const match = isWhenClause ? /\bthen\s+(else|end)\b/i.exec(clauseText) : /\belse\s+(end)\b/i.exec(clauseText);
|
|
1398
|
+
if (match) {
|
|
1399
|
+
const marker = match[1];
|
|
1400
|
+
if (marker.toLowerCase() === 'end' && recovery.from !== recovery.to) {
|
|
1401
|
+
clause = clause.nextSibling;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
const markerOffset = match.index + match[0].toLowerCase().lastIndexOf(marker.toLowerCase());
|
|
1405
|
+
const from = clause.from + markerOffset;
|
|
1406
|
+
const message = isWhenClause ? 'CASE result expected after "THEN"' : 'CASE result expected after "ELSE"';
|
|
1407
|
+
const key = `${from}:${message}`;
|
|
1408
|
+
if (!seen.has(key)) {
|
|
1409
|
+
diagnostics.push(this.makeDiagnosticAt(from, from + marker.length, message, `CaseResult:${from}`));
|
|
1410
|
+
seen.add(key);
|
|
1411
|
+
}
|
|
1412
|
+
this.caseResultRecoveryErrors.add(this.nodeRangeKey(recovery));
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
clause = clause.nextSibling;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
this.caseResultDiagnostics = diagnostics;
|
|
1419
|
+
return diagnostics;
|
|
1420
|
+
}
|
|
1421
|
+
nodeRangeKey(node) {
|
|
1422
|
+
return `${node.from}:${node.to}`;
|
|
1423
|
+
}
|
|
1424
|
+
isOperator(node) {
|
|
1425
|
+
return [
|
|
1426
|
+
'Pow',
|
|
1427
|
+
'Mul',
|
|
1428
|
+
'Div',
|
|
1429
|
+
'Mod',
|
|
1430
|
+
'Add',
|
|
1431
|
+
'Sub',
|
|
1432
|
+
'Eql',
|
|
1433
|
+
'EqlSingle',
|
|
1434
|
+
'Neq',
|
|
1435
|
+
'Lte',
|
|
1436
|
+
'Lss',
|
|
1437
|
+
'Gte',
|
|
1438
|
+
'Gtr',
|
|
1439
|
+
'EqlRegex',
|
|
1440
|
+
'NeqRegex',
|
|
1441
|
+
'In',
|
|
1442
|
+
'NotInSeq',
|
|
1443
|
+
'And',
|
|
1444
|
+
'Or',
|
|
1445
|
+
'DAnd',
|
|
1446
|
+
'DOr',
|
|
1447
|
+
].includes(node.name);
|
|
1448
|
+
}
|
|
1449
|
+
getDelimiterIssue() {
|
|
1450
|
+
if (this.delimiterIssue === undefined) {
|
|
1451
|
+
this.delimiterIssue = this.findDelimiterIssue(this.tree.topNode.to);
|
|
1452
|
+
}
|
|
1453
|
+
return this.delimiterIssue;
|
|
1454
|
+
}
|
|
1455
|
+
makeDelimiterDiagnostic(issue) {
|
|
1456
|
+
if (issue.kind === 'quote' || issue.kind === 'quotedQuery') {
|
|
1457
|
+
const context = issue.kind === 'quote' ? 'string literal' : 'quoted query';
|
|
1458
|
+
const message = issue.kind === 'quote' && issue.newline
|
|
1459
|
+
? 'unescaped newline in string literal'
|
|
1460
|
+
: `unterminated ${context}; expected closing ${this.quoteName(issue.quote)}`;
|
|
1461
|
+
return this.makeDiagnosticAt(issue.from, issue.from + 1, message, `delimiter:${issue.from}`);
|
|
1462
|
+
}
|
|
1463
|
+
if (issue.kind === 'mismatched') {
|
|
1464
|
+
const message = issue.context
|
|
1465
|
+
? `expected ${this.quoteName(issue.close)} instead of ${this.quoteName(issue.actual)} to close ${issue.context}`
|
|
1466
|
+
: issue.recovery === 'delete'
|
|
1467
|
+
? `unexpected closing delimiter "${issue.actual}" before "${issue.close}"`
|
|
1468
|
+
: `expected "${issue.close}" instead of "${issue.actual}" to close "${issue.open}"`;
|
|
1469
|
+
return this.makeDiagnosticAt(issue.actualFrom, issue.actualFrom + 1, message, `delimiter:${issue.from}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (issue.kind === 'unexpectedClosing') {
|
|
1472
|
+
return this.makeDiagnosticAt(issue.actualFrom, issue.actualFrom + 1, `unexpected closing delimiter "${issue.actual}"`, `delimiter:${issue.actualFrom}`);
|
|
1473
|
+
}
|
|
1474
|
+
return this.makeDiagnosticAt(issue.at, issue.at, `expected "${issue.close}" to close "${issue.open}"`, `delimiter:${issue.from}`);
|
|
1475
|
+
}
|
|
1476
|
+
isRelatedDelimiterError(node, issue) {
|
|
1477
|
+
if (issue.kind === 'quote' || issue.kind === 'quotedQuery') {
|
|
1478
|
+
if (node.from !== node.to) {
|
|
1479
|
+
return node.from < issue.end && node.to > issue.from;
|
|
1480
|
+
}
|
|
1481
|
+
const includesEnd = issue.kind === 'quotedQuery' || !issue.newline;
|
|
1482
|
+
return node.from >= issue.from && (node.from < issue.end || (includesEnd && node.from === issue.end));
|
|
1483
|
+
}
|
|
1484
|
+
return node.from === issue.actualFrom || (node.from <= issue.actualFrom && node.to > issue.actualFrom) || node.from === this.tree.topNode.to;
|
|
1485
|
+
}
|
|
1486
|
+
quoteName(quote) {
|
|
1487
|
+
return quote === '"' ? 'double quote' : quote === "'" ? 'single quote' : 'backtick';
|
|
1488
|
+
}
|
|
1489
|
+
findQuotedQueryDelimiterInfo() {
|
|
1490
|
+
var _a;
|
|
1491
|
+
if (this.quotedQueryDelimiterInfo)
|
|
1492
|
+
return this.quotedQueryDelimiterInfo;
|
|
1493
|
+
const transparent = new Set();
|
|
1494
|
+
const issues = new Map();
|
|
1495
|
+
const fallbackRanges = [];
|
|
1496
|
+
const fallbackDiagnostics = [];
|
|
1497
|
+
const addFallbackRange = (range) => {
|
|
1498
|
+
if (range && !fallbackRanges.some((existing) => existing.from === range.from && existing.to === range.to)) {
|
|
1499
|
+
fallbackRanges.push(range);
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
let pending = null;
|
|
1503
|
+
const cursor = this.tree.cursor();
|
|
1504
|
+
while (cursor.next()) {
|
|
1505
|
+
if (Date.now() >= this.deadline)
|
|
1506
|
+
break;
|
|
1507
|
+
if (cursor.name === 'StringLiteral') {
|
|
1508
|
+
addFallbackRange(this.findFallbackQuotedQueryRange(cursor.node));
|
|
1509
|
+
}
|
|
1510
|
+
if (cursor.name === 'Queted' && ((_a = cursor.node.parent) === null || _a === void 0 ? void 0 : _a.name) !== 'OuterArgItem') {
|
|
1511
|
+
addFallbackRange(this.findFallbackQuotedQueryRangeFromOpening(cursor.from));
|
|
1512
|
+
}
|
|
1513
|
+
if (cursor.name !== 'OuterArgItem') {
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
const quoted = [];
|
|
1517
|
+
let child = cursor.node.firstChild;
|
|
1518
|
+
while (child) {
|
|
1519
|
+
if (child.name === 'Queted')
|
|
1520
|
+
quoted.push(child);
|
|
1521
|
+
child = child.nextSibling;
|
|
1522
|
+
}
|
|
1523
|
+
for (const quote of quoted)
|
|
1524
|
+
transparent.add(quote.from);
|
|
1525
|
+
if (quoted.length >= 2) {
|
|
1526
|
+
const first = quoted[0];
|
|
1527
|
+
const last = quoted[quoted.length - 1];
|
|
1528
|
+
const expected = this.nodeText(first);
|
|
1529
|
+
const actual = this.nodeText(last);
|
|
1530
|
+
if (expected !== actual) {
|
|
1531
|
+
issues.set(last.from, {
|
|
1532
|
+
kind: 'mismatched',
|
|
1533
|
+
open: expected,
|
|
1534
|
+
close: expected,
|
|
1535
|
+
from: first.from,
|
|
1536
|
+
actual,
|
|
1537
|
+
actualFrom: last.from,
|
|
1538
|
+
context: 'quoted query',
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
const range = { from: first.from, to: last.to };
|
|
1543
|
+
const payload = this.state.sliceDoc(first.to, last.from);
|
|
1544
|
+
if (this.repairScopedDelimiterMismatches(payload) !== null && this.isFallbackQuotedQueryRange(range.from, range.to)) {
|
|
1545
|
+
addFallbackRange(range);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
else if (quoted.length === 1) {
|
|
1550
|
+
const quote = quoted[0];
|
|
1551
|
+
const wrapperQuote = this.nodeText(quote);
|
|
1552
|
+
const lexicalClosing = this.findLexicalQuotedQueryClosing(this.state.doc.toString(), quote.from, wrapperQuote);
|
|
1553
|
+
const fallbackRange = lexicalClosing === null ? null : { from: quote.from, to: lexicalClosing + 1 };
|
|
1554
|
+
if (fallbackRange && this.isFallbackQuotedQueryRange(fallbackRange.from, fallbackRange.to)) {
|
|
1555
|
+
addFallbackRange(fallbackRange);
|
|
1556
|
+
}
|
|
1557
|
+
else {
|
|
1558
|
+
const issue = {
|
|
1559
|
+
kind: 'quotedQuery',
|
|
1560
|
+
quote: wrapperQuote,
|
|
1561
|
+
from: quote.from,
|
|
1562
|
+
end: cursor.to,
|
|
1563
|
+
repairAt: cursor.to,
|
|
1564
|
+
};
|
|
1565
|
+
if (!pending || issue.from < pending.from)
|
|
1566
|
+
pending = issue;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
fallbackRanges.sort((left, right) => left.from - right.from || right.to - left.to);
|
|
1571
|
+
const disjointFallbackRanges = [];
|
|
1572
|
+
for (const range of fallbackRanges) {
|
|
1573
|
+
const previous = disjointFallbackRanges[disjointFallbackRanges.length - 1];
|
|
1574
|
+
if (!previous || range.from >= previous.to)
|
|
1575
|
+
disjointFallbackRanges.push(range);
|
|
1576
|
+
}
|
|
1577
|
+
fallbackRanges.splice(0, fallbackRanges.length, ...disjointFallbackRanges);
|
|
1578
|
+
for (const range of fallbackRanges) {
|
|
1579
|
+
transparent.add(range.from);
|
|
1580
|
+
transparent.add(range.to - 1);
|
|
1581
|
+
}
|
|
1582
|
+
fallbackDiagnostics.push(...this.diagnoseFallbackQuotedQueries(fallbackRanges));
|
|
1583
|
+
this.quotedQueryDelimiterInfo = { transparent, issues, pending, fallbackRanges, fallbackDiagnostics };
|
|
1584
|
+
return this.quotedQueryDelimiterInfo;
|
|
1585
|
+
}
|
|
1586
|
+
findFallbackQuotedQueryRange(node) {
|
|
1587
|
+
var _a;
|
|
1588
|
+
const indexItem = node.parent;
|
|
1589
|
+
const indexExpr = indexItem === null || indexItem === void 0 ? void 0 : indexItem.parent;
|
|
1590
|
+
const namespace = indexExpr === null || indexExpr === void 0 ? void 0 : indexExpr.parent;
|
|
1591
|
+
if ((indexItem === null || indexItem === void 0 ? void 0 : indexItem.name) !== 'IndexItem' || (indexExpr === null || indexExpr === void 0 ? void 0 : indexExpr.name) !== 'IndexExpr' || (namespace === null || namespace === void 0 ? void 0 : namespace.name) !== 'Namespace') {
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
if (indexItem.prevSibling)
|
|
1595
|
+
return null;
|
|
1596
|
+
const namespaceParent = namespace.parent;
|
|
1597
|
+
if ((namespaceParent === null || namespaceParent === void 0 ? void 0 : namespaceParent.name) !== 'QueryExpr' && (namespaceParent === null || namespaceParent === void 0 ? void 0 : namespaceParent.name) !== 'Query')
|
|
1598
|
+
return null;
|
|
1599
|
+
if (namespaceParent.name === 'Query' && ((_a = this.siblingSkippingLineComments(namespace, 'next')) === null || _a === void 0 ? void 0 : _a.name) === 'DColon')
|
|
1600
|
+
return null;
|
|
1601
|
+
const raw = this.nodeText(node);
|
|
1602
|
+
const quote = raw[0];
|
|
1603
|
+
if (!['"', "'", '`'].includes(quote))
|
|
1604
|
+
return null;
|
|
1605
|
+
const text = this.state.doc.toString();
|
|
1606
|
+
const lexicalClosing = this.findLexicalQuotedQueryClosing(text, node.from, quote);
|
|
1607
|
+
const candidates = [lexicalClosing === null ? null : lexicalClosing + 1, raw.length >= 2 && raw[raw.length - 1] === quote ? node.to : null];
|
|
1608
|
+
for (const to of [...new Set(candidates)]) {
|
|
1609
|
+
if (to === null || to <= node.from + 1 || this.state.sliceDoc(to - 1, to) !== quote)
|
|
1610
|
+
continue;
|
|
1611
|
+
if (this.isFallbackQuotedQueryRange(node.from, to))
|
|
1612
|
+
return { from: node.from, to };
|
|
1613
|
+
}
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
findFallbackQuotedQueryRangeFromOpening(from) {
|
|
1617
|
+
const quote = this.state.sliceDoc(from, from + 1);
|
|
1618
|
+
if (!['"', "'", '`'].includes(quote))
|
|
1619
|
+
return null;
|
|
1620
|
+
const closing = this.findLexicalQuotedQueryClosing(this.state.doc.toString(), from, quote);
|
|
1621
|
+
if (closing === null || closing <= from)
|
|
1622
|
+
return null;
|
|
1623
|
+
const to = closing + 1;
|
|
1624
|
+
return this.isFallbackQuotedQueryRange(from, to) ? { from, to } : null;
|
|
1625
|
+
}
|
|
1626
|
+
isFallbackQuotedQueryRange(from, to) {
|
|
1627
|
+
const originalPayload = this.state.sliceDoc(from + 1, to - 1);
|
|
1628
|
+
if (!this.formsOuterQuotedQuery(from, to, 'M::cpu:(usage)'))
|
|
1629
|
+
return false;
|
|
1630
|
+
if (this.isQueryPayload(originalPayload))
|
|
1631
|
+
return true;
|
|
1632
|
+
const bulkRepairedPayload = this.repairScopedDelimiterMismatches(originalPayload);
|
|
1633
|
+
if (bulkRepairedPayload && this.isQueryPayload(bulkRepairedPayload))
|
|
1634
|
+
return true;
|
|
1635
|
+
const queue = [{ payload: originalPayload, depth: 0 }];
|
|
1636
|
+
const seen = new Set();
|
|
1637
|
+
while (queue.length > 0 && Date.now() < this.deadline) {
|
|
1638
|
+
const candidate = queue.shift();
|
|
1639
|
+
if (seen.has(candidate.payload))
|
|
1640
|
+
continue;
|
|
1641
|
+
seen.add(candidate.payload);
|
|
1642
|
+
const issue = this.findFirstDelimiterMismatch(candidate.payload);
|
|
1643
|
+
if (!issue) {
|
|
1644
|
+
if (candidate.depth > 0 && this.isQueryPayload(candidate.payload))
|
|
1645
|
+
return true;
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (candidate.depth >= maxMismatchRepairDepth + 1)
|
|
1649
|
+
continue;
|
|
1650
|
+
queue.push({
|
|
1651
|
+
payload: candidate.payload.slice(0, issue.from) + candidate.payload.slice(issue.from + 1),
|
|
1652
|
+
depth: candidate.depth + 1,
|
|
1653
|
+
});
|
|
1654
|
+
if (issue.expected) {
|
|
1655
|
+
queue.push({
|
|
1656
|
+
payload: candidate.payload.slice(0, issue.from) + issue.expected + candidate.payload.slice(issue.from + 1),
|
|
1657
|
+
depth: candidate.depth + 1,
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
isQueryPayload(payload) {
|
|
1664
|
+
var _a;
|
|
1665
|
+
const payloadState = this.state.update({ changes: { from: 0, to: this.state.doc.length, insert: payload }, filter: false }).state;
|
|
1666
|
+
const payloadTree = this.getCompleteTree(payloadState);
|
|
1667
|
+
if (!payloadTree)
|
|
1668
|
+
return false;
|
|
1669
|
+
const cursor = payloadTree.cursor();
|
|
1670
|
+
while (cursor.next()) {
|
|
1671
|
+
if (cursor.name !== 'Query' || ((_a = cursor.node.parent) === null || _a === void 0 ? void 0 : _a.name) !== 'QueryExpr')
|
|
1672
|
+
continue;
|
|
1673
|
+
return !this.containsError(cursor.node) || Boolean(cursor.node.getChild('QueryClause'));
|
|
1674
|
+
}
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
diagnoseFallbackQuotedQueries(ranges) {
|
|
1678
|
+
if (ranges.length === 0)
|
|
1679
|
+
return [];
|
|
1680
|
+
const diagnostics = [];
|
|
1681
|
+
const canonicalPayload = 'M::cpu:(usage)';
|
|
1682
|
+
for (const range of ranges) {
|
|
1683
|
+
if (Date.now() >= this.deadline)
|
|
1684
|
+
return diagnostics;
|
|
1685
|
+
const payload = this.state.sliceDoc(range.from + 1, range.to - 1);
|
|
1686
|
+
const payloadState = this.state.update({ changes: { from: 0, to: this.state.doc.length, insert: payload }, filter: false }).state;
|
|
1687
|
+
const payloadParser = new Parser(payloadState, this.deadline);
|
|
1688
|
+
payloadParser.repairDepth = this.repairDepth;
|
|
1689
|
+
payloadParser.mismatchRepairDepth = this.mismatchRepairDepth;
|
|
1690
|
+
payloadParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
1691
|
+
payloadParser.analyze();
|
|
1692
|
+
for (const diagnostic of payloadParser.getDiagnostics()) {
|
|
1693
|
+
const mapped = {
|
|
1694
|
+
...diagnostic,
|
|
1695
|
+
from: diagnostic.from + range.from + 1,
|
|
1696
|
+
to: diagnostic.to + range.from + 1,
|
|
1697
|
+
};
|
|
1698
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, mapped)))
|
|
1699
|
+
diagnostics.push(mapped);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (Date.now() >= this.deadline)
|
|
1703
|
+
return diagnostics;
|
|
1704
|
+
const changes = ranges.map((range) => ({ from: range.from + 1, to: range.to - 1, insert: canonicalPayload }));
|
|
1705
|
+
const probeTransaction = this.state.update({ changes, filter: false });
|
|
1706
|
+
const insertedRanges = [];
|
|
1707
|
+
let offset = 0;
|
|
1708
|
+
for (const range of ranges) {
|
|
1709
|
+
const from = range.from + 1 + offset;
|
|
1710
|
+
insertedRanges.push({ from, to: from + canonicalPayload.length });
|
|
1711
|
+
offset += canonicalPayload.length - (range.to - range.from - 2);
|
|
1712
|
+
}
|
|
1713
|
+
const probeParser = new Parser(probeTransaction.state, this.deadline);
|
|
1714
|
+
probeParser.repairDepth = this.repairDepth;
|
|
1715
|
+
probeParser.mismatchRepairDepth = this.mismatchRepairDepth;
|
|
1716
|
+
probeParser.terminalRepairUsed = this.terminalRepairUsed;
|
|
1717
|
+
probeParser.analyze();
|
|
1718
|
+
const outerDiagnostics = probeParser.getDiagnostics().filter((diagnostic) => !insertedRanges.some((range) => diagnostic.from === diagnostic.to
|
|
1719
|
+
? diagnostic.from >= range.from && diagnostic.from <= range.to
|
|
1720
|
+
: diagnostic.from < range.to && diagnostic.to > range.from));
|
|
1721
|
+
for (const diagnostic of this.mapDiagnosticsThroughChanges(outerDiagnostics, probeTransaction.changes)) {
|
|
1722
|
+
if (!diagnostics.some((existing) => this.isSameDiagnostic(existing, diagnostic)))
|
|
1723
|
+
diagnostics.push(diagnostic);
|
|
1724
|
+
}
|
|
1725
|
+
return diagnostics;
|
|
1726
|
+
}
|
|
1727
|
+
repairScopedDelimiterMismatches(text) {
|
|
1728
|
+
var _a, _b, _c;
|
|
1729
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
1730
|
+
const closers = new Set(Object.values(pairs));
|
|
1731
|
+
const stack = [];
|
|
1732
|
+
const stackCloserCounts = new Map();
|
|
1733
|
+
const futureCloserCounts = new Map();
|
|
1734
|
+
const changes = [];
|
|
1735
|
+
const transparent = new Set();
|
|
1736
|
+
let quote = '';
|
|
1737
|
+
let escaped = false;
|
|
1738
|
+
let inComment = false;
|
|
1739
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1740
|
+
if (Date.now() >= this.deadline)
|
|
1741
|
+
return null;
|
|
1742
|
+
const char = text[index];
|
|
1743
|
+
if (inComment) {
|
|
1744
|
+
if (char === '\n')
|
|
1745
|
+
inComment = false;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (quote) {
|
|
1749
|
+
if (escaped) {
|
|
1750
|
+
escaped = false;
|
|
1751
|
+
}
|
|
1752
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
1753
|
+
escaped = true;
|
|
1754
|
+
}
|
|
1755
|
+
else if (char === quote) {
|
|
1756
|
+
quote = '';
|
|
1757
|
+
}
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
1761
|
+
quote = char;
|
|
1762
|
+
}
|
|
1763
|
+
else if (char === '#') {
|
|
1764
|
+
inComment = true;
|
|
1765
|
+
}
|
|
1766
|
+
else if (pairs[char]) {
|
|
1767
|
+
const closer = pairs[char];
|
|
1768
|
+
stack.push(closer);
|
|
1769
|
+
stackCloserCounts.set(closer, ((_a = stackCloserCounts.get(closer)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
1770
|
+
futureCloserCounts.clear();
|
|
1771
|
+
}
|
|
1772
|
+
else if (closers.has(char)) {
|
|
1773
|
+
const expected = stack[stack.length - 1];
|
|
1774
|
+
if (char === expected) {
|
|
1775
|
+
stack.pop();
|
|
1776
|
+
stackCloserCounts.set(expected, ((_b = stackCloserCounts.get(expected)) !== null && _b !== void 0 ? _b : 1) - 1);
|
|
1777
|
+
futureCloserCounts.clear();
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (!expected) {
|
|
1781
|
+
changes.push({ from: index, to: index + 1, insert: '' });
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const sameCloserDepth = (_c = stackCloserCounts.get(expected)) !== null && _c !== void 0 ? _c : 0;
|
|
1785
|
+
let futureCloserCount = futureCloserCounts.get(expected);
|
|
1786
|
+
if (futureCloserCount === undefined) {
|
|
1787
|
+
const countedClosers = this.countFutureClosersAtCurrentDepth(text, index + 1, expected, transparent);
|
|
1788
|
+
if (countedClosers === null)
|
|
1789
|
+
return null;
|
|
1790
|
+
futureCloserCount = countedClosers;
|
|
1791
|
+
futureCloserCounts.set(expected, countedClosers);
|
|
1792
|
+
}
|
|
1793
|
+
if (futureCloserCount >= sameCloserDepth) {
|
|
1794
|
+
changes.push({ from: index, to: index + 1, insert: '' });
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
changes.push({ from: index, to: index + 1, insert: expected });
|
|
1798
|
+
stack.pop();
|
|
1799
|
+
stackCloserCounts.set(expected, sameCloserDepth - 1);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (quote)
|
|
1804
|
+
return null;
|
|
1805
|
+
const missingClosers = [...stack].reverse().join('');
|
|
1806
|
+
if (changes.length === 0 && !missingClosers)
|
|
1807
|
+
return null;
|
|
1808
|
+
let repaired = '';
|
|
1809
|
+
let copiedTo = 0;
|
|
1810
|
+
for (const change of changes) {
|
|
1811
|
+
repaired += text.slice(copiedTo, change.from) + change.insert;
|
|
1812
|
+
copiedTo = change.to;
|
|
1813
|
+
}
|
|
1814
|
+
return repaired + text.slice(copiedTo) + missingClosers;
|
|
1815
|
+
}
|
|
1816
|
+
findFirstDelimiterMismatch(text) {
|
|
1817
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
1818
|
+
const closers = new Set(Object.values(pairs));
|
|
1819
|
+
const stack = [];
|
|
1820
|
+
let quote = '';
|
|
1821
|
+
let escaped = false;
|
|
1822
|
+
let inComment = false;
|
|
1823
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1824
|
+
const char = text[index];
|
|
1825
|
+
if (inComment) {
|
|
1826
|
+
if (char === '\n')
|
|
1827
|
+
inComment = false;
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (quote) {
|
|
1831
|
+
if (escaped) {
|
|
1832
|
+
escaped = false;
|
|
1833
|
+
}
|
|
1834
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
1835
|
+
escaped = true;
|
|
1836
|
+
}
|
|
1837
|
+
else if (char === quote) {
|
|
1838
|
+
quote = '';
|
|
1839
|
+
}
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
1843
|
+
quote = char;
|
|
1844
|
+
}
|
|
1845
|
+
else if (char === '#') {
|
|
1846
|
+
inComment = true;
|
|
1847
|
+
}
|
|
1848
|
+
else if (pairs[char]) {
|
|
1849
|
+
stack.push(pairs[char]);
|
|
1850
|
+
}
|
|
1851
|
+
else if (closers.has(char)) {
|
|
1852
|
+
const expected = stack[stack.length - 1];
|
|
1853
|
+
if (char !== expected)
|
|
1854
|
+
return { from: index, expected };
|
|
1855
|
+
stack.pop();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
formsOuterQuotedQuery(from, to, payload) {
|
|
1861
|
+
const repairedState = this.state.update({ changes: { from: from + 1, to: to - 1, insert: payload }, filter: false }).state;
|
|
1862
|
+
const repairedTree = this.getCompleteTree(repairedState);
|
|
1863
|
+
if (!repairedTree)
|
|
1864
|
+
return false;
|
|
1865
|
+
const wrapperQuote = this.state.sliceDoc(from, from + 1);
|
|
1866
|
+
const expectedClosing = from + payload.length + 1;
|
|
1867
|
+
const cursor = repairedTree.cursor();
|
|
1868
|
+
while (cursor.next()) {
|
|
1869
|
+
if (cursor.name !== 'OuterArgItem' || cursor.from !== from)
|
|
1870
|
+
continue;
|
|
1871
|
+
const query = cursor.node.getChild('Query');
|
|
1872
|
+
const quotes = cursor.node.getChildren('Queted');
|
|
1873
|
+
const openingQuote = quotes.find((quote) => quote.from === from);
|
|
1874
|
+
const closingQuote = quotes.find((quote) => quote.from === expectedClosing);
|
|
1875
|
+
if (query &&
|
|
1876
|
+
openingQuote &&
|
|
1877
|
+
closingQuote &&
|
|
1878
|
+
repairedState.sliceDoc(openingQuote.from, openingQuote.to) === wrapperQuote &&
|
|
1879
|
+
repairedState.sliceDoc(closingQuote.from, closingQuote.to) === wrapperQuote) {
|
|
1880
|
+
return true;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
findDelimiterIssue(end, scoreRecovery = true) {
|
|
1886
|
+
var _a, _b, _c, _d, _e;
|
|
1887
|
+
const text = this.state.sliceDoc(0, end);
|
|
1888
|
+
const stack = [];
|
|
1889
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
1890
|
+
const closingDelimiters = new Set(Object.values(pairs));
|
|
1891
|
+
let quote = '';
|
|
1892
|
+
let quoteFrom = -1;
|
|
1893
|
+
let quoteRepairCandidates = [];
|
|
1894
|
+
let escaped = false;
|
|
1895
|
+
const quotedQueryDelimiters = this.findQuotedQueryDelimiterInfo();
|
|
1896
|
+
let quotedQueryOuterDepth = -1;
|
|
1897
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1898
|
+
if ((index & 255) === 0 && Date.now() >= this.deadline)
|
|
1899
|
+
return null;
|
|
1900
|
+
const char = text[index];
|
|
1901
|
+
const fallbackRange = quotedQueryDelimiters.fallbackRanges.find((range) => index >= range.from && index < range.to);
|
|
1902
|
+
if (fallbackRange) {
|
|
1903
|
+
index = fallbackRange.to - 1;
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
const quotedQueryIssue = quotedQueryDelimiters.issues.get(index);
|
|
1907
|
+
if (quotedQueryIssue)
|
|
1908
|
+
return quotedQueryIssue;
|
|
1909
|
+
if (quotedQueryDelimiters.transparent.has(index)) {
|
|
1910
|
+
if (((_a = quotedQueryDelimiters.pending) === null || _a === void 0 ? void 0 : _a.kind) === 'quotedQuery' && quotedQueryDelimiters.pending.from === index) {
|
|
1911
|
+
quotedQueryOuterDepth = stack.length;
|
|
1912
|
+
}
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
if (quote) {
|
|
1916
|
+
if (escaped) {
|
|
1917
|
+
escaped = false;
|
|
1918
|
+
}
|
|
1919
|
+
else if (char === '\\' && quote !== '`') {
|
|
1920
|
+
escaped = true;
|
|
1921
|
+
}
|
|
1922
|
+
else if (char === quote) {
|
|
1923
|
+
quote = '';
|
|
1924
|
+
quoteFrom = -1;
|
|
1925
|
+
quoteRepairCandidates = [];
|
|
1926
|
+
}
|
|
1927
|
+
else if (char === '\n' && quote !== '`') {
|
|
1928
|
+
const quotedQueryRepairAt = this.findVerifiedQuotedQueryRepairAt(text, quoteFrom, quote, (_b = stack[stack.length - 1]) === null || _b === void 0 ? void 0 : _b.close, end);
|
|
1929
|
+
if (quotedQueryRepairAt !== null) {
|
|
1930
|
+
return {
|
|
1931
|
+
kind: 'quotedQuery',
|
|
1932
|
+
quote,
|
|
1933
|
+
from: quoteFrom,
|
|
1934
|
+
end,
|
|
1935
|
+
repairAt: quotedQueryRepairAt,
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
const recovery = this.findStringNewlineRecovery(text, quoteFrom + 1, quote);
|
|
1939
|
+
return {
|
|
1940
|
+
kind: 'quote',
|
|
1941
|
+
quote,
|
|
1942
|
+
from: quoteFrom,
|
|
1943
|
+
end: (_c = recovery === null || recovery === void 0 ? void 0 : recovery.end) !== null && _c !== void 0 ? _c : index + 1,
|
|
1944
|
+
newline: true,
|
|
1945
|
+
repairLineBreaks: recovery === null || recovery === void 0 ? void 0 : recovery.lineBreaks,
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
else if (stack.length > 0 && char === stack[stack.length - 1].close) {
|
|
1949
|
+
quoteRepairCandidates.push(index);
|
|
1950
|
+
}
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1954
|
+
quote = char;
|
|
1955
|
+
quoteFrom = index;
|
|
1956
|
+
quoteRepairCandidates = [];
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (char === '#') {
|
|
1960
|
+
while (index + 1 < text.length && text[index + 1] !== '\n') {
|
|
1961
|
+
index += 1;
|
|
1962
|
+
}
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
if (pairs[char]) {
|
|
1966
|
+
stack.push({ open: char, close: pairs[char], from: index });
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
const last = stack[stack.length - 1];
|
|
1970
|
+
if (last && char === last.close) {
|
|
1971
|
+
if (((_d = quotedQueryDelimiters.pending) === null || _d === void 0 ? void 0 : _d.kind) === 'quotedQuery' &&
|
|
1972
|
+
quotedQueryOuterDepth > 0 &&
|
|
1973
|
+
stack.length === quotedQueryOuterDepth &&
|
|
1974
|
+
quotedQueryDelimiters.pending.repairAt === quotedQueryDelimiters.pending.end) {
|
|
1975
|
+
quotedQueryDelimiters.pending.repairAt = index;
|
|
1976
|
+
quotedQueryOuterDepth = -1;
|
|
1977
|
+
}
|
|
1978
|
+
stack.pop();
|
|
1979
|
+
}
|
|
1980
|
+
else if (last && closingDelimiters.has(char)) {
|
|
1981
|
+
const sameCloserDepth = stack.filter((entry) => entry.close === last.close).length;
|
|
1982
|
+
const futureCloserCount = scoreRecovery
|
|
1983
|
+
? this.countFutureClosersAtCurrentDepth(text, index + 1, last.close, quotedQueryDelimiters.transparent)
|
|
1984
|
+
: 0;
|
|
1985
|
+
return {
|
|
1986
|
+
kind: 'mismatched',
|
|
1987
|
+
open: last.open,
|
|
1988
|
+
close: last.close,
|
|
1989
|
+
from: last.from,
|
|
1990
|
+
actual: char,
|
|
1991
|
+
actualFrom: index,
|
|
1992
|
+
recovery: scoreRecovery
|
|
1993
|
+
? futureCloserCount !== null && futureCloserCount >= sameCloserDepth
|
|
1994
|
+
? 'delete'
|
|
1995
|
+
: Date.now() >= this.deadline
|
|
1996
|
+
? 'replace'
|
|
1997
|
+
: this.chooseMismatchRecovery(index, index + 1, last.close)
|
|
1998
|
+
: 'replace',
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
else if (!last && closingDelimiters.has(char) && !this.hasPrecedingErrorEndingAt(index)) {
|
|
2002
|
+
return { kind: 'unexpectedClosing', actual: char, actualFrom: index };
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (quote) {
|
|
2006
|
+
return {
|
|
2007
|
+
kind: 'quote',
|
|
2008
|
+
quote,
|
|
2009
|
+
from: quoteFrom,
|
|
2010
|
+
end,
|
|
2011
|
+
repairAt: this.chooseQuoteRepairAt(text, quoteFrom, quote, (_e = stack[stack.length - 1]) === null || _e === void 0 ? void 0 : _e.close, quoteRepairCandidates, end),
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
if (quotedQueryDelimiters.pending)
|
|
2015
|
+
return quotedQueryDelimiters.pending;
|
|
2016
|
+
const unclosed = stack[stack.length - 1];
|
|
2017
|
+
return unclosed ? { kind: 'unclosed', ...unclosed, at: end } : null;
|
|
2018
|
+
}
|
|
2019
|
+
hasPrecedingErrorEndingAt(position) {
|
|
2020
|
+
const cursor = this.tree.cursor();
|
|
2021
|
+
while (cursor.next()) {
|
|
2022
|
+
if (cursor.type.isError && cursor.from < position && cursor.to === position)
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
countFutureClosersAtCurrentDepth(text, start, closer, transparent) {
|
|
2028
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
2029
|
+
const closingDelimiters = new Set(Object.values(pairs));
|
|
2030
|
+
const nestedClosers = [];
|
|
2031
|
+
let quote = '';
|
|
2032
|
+
let escaped = false;
|
|
2033
|
+
let inComment = false;
|
|
2034
|
+
let count = 0;
|
|
2035
|
+
for (let index = start; index < text.length; index += 1) {
|
|
2036
|
+
if (Date.now() >= this.deadline)
|
|
2037
|
+
return null;
|
|
2038
|
+
const char = text[index];
|
|
2039
|
+
if (transparent.has(index))
|
|
2040
|
+
continue;
|
|
2041
|
+
if (inComment) {
|
|
2042
|
+
if (char === '\n')
|
|
2043
|
+
inComment = false;
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
if (quote) {
|
|
2047
|
+
if (escaped) {
|
|
2048
|
+
escaped = false;
|
|
2049
|
+
}
|
|
2050
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
2051
|
+
escaped = true;
|
|
2052
|
+
}
|
|
2053
|
+
else if (char === quote) {
|
|
2054
|
+
quote = '';
|
|
2055
|
+
}
|
|
2056
|
+
else if (char === '\n' && quote.charCodeAt(0) !== 96) {
|
|
2057
|
+
quote = '';
|
|
2058
|
+
}
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
2062
|
+
quote = char;
|
|
2063
|
+
}
|
|
2064
|
+
else if (char === '#') {
|
|
2065
|
+
inComment = true;
|
|
2066
|
+
}
|
|
2067
|
+
else if (pairs[char]) {
|
|
2068
|
+
nestedClosers.push(pairs[char]);
|
|
2069
|
+
}
|
|
2070
|
+
else if (closingDelimiters.has(char)) {
|
|
2071
|
+
if (nestedClosers.length > 0) {
|
|
2072
|
+
nestedClosers.pop();
|
|
2073
|
+
}
|
|
2074
|
+
else if (char === closer) {
|
|
2075
|
+
count += 1;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return count;
|
|
2080
|
+
}
|
|
2081
|
+
chooseQuoteRepairAt(text, quoteFrom, quote, expectedCloser, closerCandidates, end) {
|
|
2082
|
+
var _a;
|
|
2083
|
+
const quotedQueryRepairAt = this.findVerifiedQuotedQueryRepairAt(text, quoteFrom, quote, expectedCloser, end);
|
|
2084
|
+
if (quotedQueryRepairAt !== null)
|
|
2085
|
+
return quotedQueryRepairAt;
|
|
2086
|
+
const ordinaryPositions = closerCandidates.length > 0 ? [...new Set(closerCandidates)].sort((left, right) => left - right) : [end];
|
|
2087
|
+
const repairs = [];
|
|
2088
|
+
for (let index = 0; index < ordinaryPositions.length && Date.now() < this.deadline; index += 1) {
|
|
2089
|
+
const repair = this.scoreQuoteRepairCandidate(ordinaryPositions[index], quote, quoteFrom);
|
|
2090
|
+
repairs.push(repair);
|
|
2091
|
+
const localEnd = ((_a = ordinaryPositions[index + 1]) !== null && _a !== void 0 ? _a : ordinaryPositions[index]) + quote.length;
|
|
2092
|
+
if (repair.complete && !repair.errorPositions.some(({ from, to }) => from <= localEnd && to >= quoteFrom)) {
|
|
2093
|
+
return repair.insertAt;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
if (repairs.length < ordinaryPositions.length)
|
|
2097
|
+
return undefined;
|
|
2098
|
+
if (!repairs.some(({ complete }) => complete))
|
|
2099
|
+
return end;
|
|
2100
|
+
return this.pickQuoteRepair(repairs, end);
|
|
2101
|
+
}
|
|
2102
|
+
findVerifiedQuotedQueryRepairAt(text, quoteFrom, quote, expectedCloser, end) {
|
|
2103
|
+
const candidate = expectedCloser ? this.findQuotedQueryRepairCandidate(text, quoteFrom + 1, quote, expectedCloser) : null;
|
|
2104
|
+
const positions = [...new Set([candidate, end].filter((position) => position !== null))];
|
|
2105
|
+
const repairs = positions.map((insertAt) => this.scoreQuoteRepairCandidate(insertAt, quote, quoteFrom)).filter((repair) => repair.quotedQuery);
|
|
2106
|
+
return repairs.length > 0 ? this.pickQuoteRepair(repairs, end) : null;
|
|
2107
|
+
}
|
|
2108
|
+
scoreQuoteRepairCandidate(insertAt, quote, quoteFrom) {
|
|
2109
|
+
if (Date.now() >= this.deadline) {
|
|
2110
|
+
return { insertAt, errors: Number.POSITIVE_INFINITY, errorPositions: [], quotedQuery: false, complete: false };
|
|
2111
|
+
}
|
|
2112
|
+
const state = this.state.update({ changes: { from: insertAt, insert: quote }, filter: false }).state;
|
|
2113
|
+
const tree = this.getCompleteTree(state);
|
|
2114
|
+
if (!tree) {
|
|
2115
|
+
return { insertAt, errors: Number.POSITIVE_INFINITY, errorPositions: [], quotedQuery: false, complete: false };
|
|
2116
|
+
}
|
|
2117
|
+
const cursor = tree.cursor();
|
|
2118
|
+
let errors = 0;
|
|
2119
|
+
const errorPositions = [];
|
|
2120
|
+
let quotedQuery = false;
|
|
2121
|
+
while (cursor.next()) {
|
|
2122
|
+
if (cursor.type.isError) {
|
|
2123
|
+
errors += 1;
|
|
2124
|
+
errorPositions.push({ from: cursor.from, to: cursor.to });
|
|
2125
|
+
}
|
|
2126
|
+
if (cursor.name !== 'OuterArgItem')
|
|
2127
|
+
continue;
|
|
2128
|
+
const quoted = [];
|
|
2129
|
+
let child = cursor.node.firstChild;
|
|
2130
|
+
while (child) {
|
|
2131
|
+
if (child.name === 'Queted')
|
|
2132
|
+
quoted.push(child);
|
|
2133
|
+
child = child.nextSibling;
|
|
2134
|
+
}
|
|
2135
|
+
if (quoted.length >= 2 && quoted[0].from === quoteFrom && quoted.some((node) => node.from === insertAt)) {
|
|
2136
|
+
quotedQuery = true;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return { insertAt, errors, errorPositions, quotedQuery, complete: true };
|
|
2140
|
+
}
|
|
2141
|
+
pickQuoteRepair(candidates, end) {
|
|
2142
|
+
return candidates.sort((left, right) => {
|
|
2143
|
+
if (left.errors !== right.errors)
|
|
2144
|
+
return left.errors - right.errors;
|
|
2145
|
+
const leftAtEnd = left.insertAt === end;
|
|
2146
|
+
const rightAtEnd = right.insertAt === end;
|
|
2147
|
+
if (leftAtEnd !== rightAtEnd)
|
|
2148
|
+
return leftAtEnd ? 1 : -1;
|
|
2149
|
+
return right.insertAt - left.insertAt;
|
|
2150
|
+
})[0].insertAt;
|
|
2151
|
+
}
|
|
2152
|
+
findQuotedQueryRepairCandidate(text, start, outerQuote, expectedCloser) {
|
|
2153
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
2154
|
+
const closingDelimiters = new Set(Object.values(pairs));
|
|
2155
|
+
const stack = [];
|
|
2156
|
+
let quote = '';
|
|
2157
|
+
let escaped = false;
|
|
2158
|
+
let outerEscaped = false;
|
|
2159
|
+
let inComment = false;
|
|
2160
|
+
for (let index = start; index < text.length; index += 1) {
|
|
2161
|
+
const char = text[index];
|
|
2162
|
+
if (inComment) {
|
|
2163
|
+
if (char === '\n')
|
|
2164
|
+
inComment = false;
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (quote) {
|
|
2168
|
+
if (escaped) {
|
|
2169
|
+
escaped = false;
|
|
2170
|
+
}
|
|
2171
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
2172
|
+
escaped = true;
|
|
2173
|
+
}
|
|
2174
|
+
else if (char === quote) {
|
|
2175
|
+
quote = '';
|
|
2176
|
+
}
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (outerEscaped) {
|
|
2180
|
+
outerEscaped = false;
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (char === '\\' && outerQuote.charCodeAt(0) !== 96) {
|
|
2184
|
+
outerEscaped = true;
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
2188
|
+
quote = char;
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
if (char === '#') {
|
|
2192
|
+
inComment = true;
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
if (pairs[char]) {
|
|
2196
|
+
stack.push(pairs[char]);
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
if (!closingDelimiters.has(char))
|
|
2200
|
+
continue;
|
|
2201
|
+
if (stack[stack.length - 1] === char) {
|
|
2202
|
+
stack.pop();
|
|
2203
|
+
}
|
|
2204
|
+
else if (stack.length === 0 && char === expectedCloser) {
|
|
2205
|
+
return index;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return null;
|
|
2209
|
+
}
|
|
2210
|
+
chooseMismatchRecovery(from, to, expectedCloser) {
|
|
2211
|
+
const replacement = this.state.update({ changes: { from, to, insert: expectedCloser }, filter: false }).state;
|
|
2212
|
+
const deletion = this.state.update({ changes: { from, to, insert: '' }, filter: false }).state;
|
|
2213
|
+
const replacementBalanceCost = this.delimiterBalanceCost(replacement.doc.toString());
|
|
2214
|
+
const deletionBalanceCost = this.delimiterBalanceCost(deletion.doc.toString());
|
|
2215
|
+
return deletionBalanceCost < replacementBalanceCost ? 'delete' : 'replace';
|
|
2216
|
+
}
|
|
2217
|
+
delimiterBalanceCost(text) {
|
|
2218
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
2219
|
+
const closingDelimiters = new Set(Object.values(pairs));
|
|
2220
|
+
const stack = [];
|
|
2221
|
+
let quote = '';
|
|
2222
|
+
let escaped = false;
|
|
2223
|
+
let inComment = false;
|
|
2224
|
+
let errors = 0;
|
|
2225
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
2226
|
+
if (Date.now() >= this.deadline)
|
|
2227
|
+
return Number.POSITIVE_INFINITY;
|
|
2228
|
+
const char = text[index];
|
|
2229
|
+
if (inComment) {
|
|
2230
|
+
if (char === '\n')
|
|
2231
|
+
inComment = false;
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
if (quote) {
|
|
2235
|
+
if (escaped) {
|
|
2236
|
+
escaped = false;
|
|
2237
|
+
}
|
|
2238
|
+
else if (char === '\\' && quote.charCodeAt(0) !== 96) {
|
|
2239
|
+
escaped = true;
|
|
2240
|
+
}
|
|
2241
|
+
else if (char === quote) {
|
|
2242
|
+
quote = '';
|
|
2243
|
+
}
|
|
2244
|
+
else if (char === '\n' && quote.charCodeAt(0) !== 96) {
|
|
2245
|
+
quote = '';
|
|
2246
|
+
}
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
if (char === '"' || char === "'" || char.charCodeAt(0) === 96) {
|
|
2250
|
+
quote = char;
|
|
2251
|
+
}
|
|
2252
|
+
else if (char === '#') {
|
|
2253
|
+
inComment = true;
|
|
2254
|
+
}
|
|
2255
|
+
else if (pairs[char]) {
|
|
2256
|
+
stack.push(pairs[char]);
|
|
2257
|
+
}
|
|
2258
|
+
else if (closingDelimiters.has(char)) {
|
|
2259
|
+
if (stack[stack.length - 1] === char) {
|
|
2260
|
+
stack.pop();
|
|
2261
|
+
}
|
|
2262
|
+
else {
|
|
2263
|
+
errors += 1;
|
|
2264
|
+
if (stack.length > 0)
|
|
2265
|
+
stack.pop();
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return errors + stack.length + (quote ? 1 : 0);
|
|
2270
|
+
}
|
|
2271
|
+
findStringNewlineRecovery(text, start, quote) {
|
|
2272
|
+
let escaped = false;
|
|
2273
|
+
const lineBreaks = [];
|
|
2274
|
+
for (let index = start; index < text.length; index += 1) {
|
|
2275
|
+
const char = text[index];
|
|
2276
|
+
if (escaped) {
|
|
2277
|
+
escaped = false;
|
|
2278
|
+
}
|
|
2279
|
+
else if (char === '\\') {
|
|
2280
|
+
escaped = true;
|
|
2281
|
+
}
|
|
2282
|
+
else if (char === quote) {
|
|
2283
|
+
return { end: index + 1, lineBreaks };
|
|
2284
|
+
}
|
|
2285
|
+
else if (char === '\n') {
|
|
2286
|
+
lineBreaks.push(index);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return null;
|
|
2290
|
+
}
|
|
2291
|
+
checkAST(node) {
|
|
2292
|
+
if (!node || Date.now() >= this.deadline)
|
|
2293
|
+
return;
|
|
2294
|
+
if (!node.type.isError && !this.containsError(node)) {
|
|
2295
|
+
switch (node.name) {
|
|
2296
|
+
case 'Query':
|
|
2297
|
+
case 'QueryFragment':
|
|
2298
|
+
this.checkDuplicateQueryClauses(node);
|
|
2299
|
+
break;
|
|
2300
|
+
case 'ShowExpr':
|
|
2301
|
+
this.checkDuplicateShowClauses(node);
|
|
2302
|
+
this.checkShowFunctionRequirements(node);
|
|
2303
|
+
break;
|
|
2304
|
+
case 'TimeRange':
|
|
2305
|
+
this.checkTimeRangeShape(node);
|
|
2306
|
+
break;
|
|
2307
|
+
case 'RollupIdentifier':
|
|
2308
|
+
this.checkBareRollup(node);
|
|
2309
|
+
break;
|
|
2310
|
+
case 'RollupFunctionCall':
|
|
2311
|
+
this.checkRollupFunctionCall(node);
|
|
2312
|
+
break;
|
|
2313
|
+
case 'FunctionCall':
|
|
2314
|
+
this.checkFunctionCall(node);
|
|
2315
|
+
break;
|
|
2316
|
+
case 'OuterFunctionCall':
|
|
2317
|
+
this.checkOuterFunctionCall(node);
|
|
2318
|
+
break;
|
|
2319
|
+
case 'TargetClause':
|
|
2320
|
+
this.checkProjectionAlias(node);
|
|
2321
|
+
break;
|
|
2322
|
+
case 'LimitClause':
|
|
2323
|
+
case 'OffsetClause':
|
|
2324
|
+
case 'SLimitClause':
|
|
2325
|
+
case 'SOffsetClause':
|
|
2326
|
+
this.checkIntegerClause(node);
|
|
2327
|
+
break;
|
|
2328
|
+
case 'FilterSubExpr':
|
|
2329
|
+
this.checkFilterSubqueryOperator(node);
|
|
2330
|
+
break;
|
|
2331
|
+
case 'FieldName':
|
|
2332
|
+
this.checkFieldName(node);
|
|
2333
|
+
break;
|
|
2334
|
+
case 'CaseFunctionIdentifier':
|
|
2335
|
+
this.checkCaseFunction(node);
|
|
2336
|
+
break;
|
|
2337
|
+
case 'CaseExpr':
|
|
2338
|
+
this.checkCaseResults(node);
|
|
2339
|
+
break;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
let child = node.firstChild;
|
|
2343
|
+
while (child) {
|
|
2344
|
+
this.checkAST(child);
|
|
2345
|
+
child = child.nextSibling;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
checkDuplicateQueryClauses(query) {
|
|
2349
|
+
const seen = new Set();
|
|
2350
|
+
let child = query.firstChild;
|
|
2351
|
+
while (child) {
|
|
2352
|
+
if (child.name === 'QueryClause' || child.name === 'QueryFragmentClause') {
|
|
2353
|
+
const clause = child.firstChild;
|
|
2354
|
+
if (clause) {
|
|
2355
|
+
const label = queryClauseLabels.get(clause.name);
|
|
2356
|
+
if (label) {
|
|
2357
|
+
if (seen.has(clause.name)) {
|
|
2358
|
+
this.pushSemanticDiagnostic(clause, `duplicate "${label}" clause`, `duplicate:${query.from}:${clause.name}`);
|
|
2359
|
+
}
|
|
2360
|
+
else {
|
|
2361
|
+
seen.add(clause.name);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
child = child.nextSibling;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
checkDuplicateShowClauses(show) {
|
|
2370
|
+
const seen = new Set();
|
|
2371
|
+
let child = show.firstChild;
|
|
2372
|
+
while (child) {
|
|
2373
|
+
const label = queryClauseLabels.get(child.name);
|
|
2374
|
+
if (label) {
|
|
2375
|
+
if (seen.has(child.name)) {
|
|
2376
|
+
this.pushSemanticDiagnostic(child, `duplicate "${label}" clause`, `duplicate:${show.from}:${child.name}`);
|
|
2377
|
+
}
|
|
2378
|
+
else {
|
|
2379
|
+
seen.add(child.name);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
child = child.nextSibling;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
checkIntegerClause(clause) {
|
|
2386
|
+
const number = clause.getChild('Number');
|
|
2387
|
+
if (!number)
|
|
2388
|
+
return;
|
|
2389
|
+
const value = this.numericValue(number);
|
|
2390
|
+
if (value !== null && Number.isInteger(value) && value >= 0)
|
|
2391
|
+
return;
|
|
2392
|
+
const keyword = numericClauses.get(clause.name);
|
|
2393
|
+
if (keyword)
|
|
2394
|
+
this.pushSemanticDiagnostic(number, `integer expected after "${keyword}"`, `integer:${clause.from}`);
|
|
2395
|
+
}
|
|
2396
|
+
checkTimeRangeShape(range) {
|
|
2397
|
+
var _a;
|
|
2398
|
+
const raw = this.maskLineComments(this.nodeText(range));
|
|
2399
|
+
const segments = this.splitTopLevelSegments(raw, ':');
|
|
2400
|
+
const expressions = [];
|
|
2401
|
+
let child = range.firstChild;
|
|
2402
|
+
while (child) {
|
|
2403
|
+
if (child.name === 'TimeExpr')
|
|
2404
|
+
expressions.push(child);
|
|
2405
|
+
child = child.nextSibling;
|
|
2406
|
+
}
|
|
2407
|
+
if (segments.length > 4) {
|
|
2408
|
+
this.pushSemanticDiagnostic(range, 'time range accepts at most four colon-separated values', `time-shape:${range.from}`);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (segments.every((segment) => raw.slice(segment.from, segment.to).trim().length === 0)) {
|
|
2412
|
+
this.pushSemanticDiagnostic(range, 'time range must contain at least one value', `time-shape:${range.from}`);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
2416
|
+
const segment = segments[index];
|
|
2417
|
+
const absoluteFrom = range.from + segment.from;
|
|
2418
|
+
const absoluteTo = range.from + segment.to;
|
|
2419
|
+
const segmentExpressions = expressions.filter((expression) => expression.from >= absoluteFrom && expression.to <= absoluteTo);
|
|
2420
|
+
const expressionCount = segmentExpressions.length;
|
|
2421
|
+
const hasText = raw.slice(segment.from, segment.to).trim().length > 0;
|
|
2422
|
+
if ((hasText && expressionCount !== 1) || (!hasText && expressionCount !== 0)) {
|
|
2423
|
+
this.pushSemanticDiagnosticAt(absoluteFrom, absoluteTo, 'invalid value in time range', `time-shape:${range.from}`);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
if (segments.length > 1 && expressionCount === 1) {
|
|
2427
|
+
const kind = (_a = segmentExpressions[0].firstChild) === null || _a === void 0 ? void 0 : _a.name;
|
|
2428
|
+
if (index < 2 && kind !== 'Number' && kind !== 'Duration' && kind !== 'PresetExpr') {
|
|
2429
|
+
this.pushSemanticDiagnostic(segmentExpressions[0], 'time range boundary must be a timestamp, duration, or preset', `time-shape:${range.from}`);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
if (index === 2 && kind !== 'Number' && kind !== 'Duration') {
|
|
2433
|
+
this.pushSemanticDiagnostic(segmentExpressions[0], 'time range interval must be a number or duration', `time-shape:${range.from}`);
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
if (index === 3 && kind !== 'RollupIdentifier' && kind !== 'RollupFunctionCall') {
|
|
2437
|
+
this.pushSemanticDiagnostic(segmentExpressions[0], 'time range rollup must be a supported rollup function', `time-shape:${range.from}`);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
checkBareRollup(identifier) {
|
|
2444
|
+
var _a;
|
|
2445
|
+
if (((_a = identifier.parent) === null || _a === void 0 ? void 0 : _a.name) === 'RollupFunctionCall')
|
|
2446
|
+
return;
|
|
2447
|
+
const name = this.nodeText(identifier).toLowerCase();
|
|
2448
|
+
if (name === 'ewma' || name === 'moving_average' || name === 'percentile') {
|
|
2449
|
+
this.pushSemanticDiagnostic(identifier, `rollup function "${name}" requires arguments`, `rollup:${identifier.from}`);
|
|
2450
|
+
}
|
|
2451
|
+
else if (name === 'corr') {
|
|
2452
|
+
this.pushSemanticDiagnostic(identifier, 'rollup function "corr" requires multiple input fields and is only valid in the select clause', `rollup:${identifier.from}`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
checkRollupFunctionCall(call) {
|
|
2456
|
+
const identifier = call.getChild('RollupIdentifier');
|
|
2457
|
+
const body = call.getChild('RollupFunctionCallBody');
|
|
2458
|
+
if (!identifier || !body)
|
|
2459
|
+
return;
|
|
2460
|
+
const name = this.nodeText(identifier).toLowerCase();
|
|
2461
|
+
const args = this.directChildren(body, 'RollupArg');
|
|
2462
|
+
if (name === 'corr') {
|
|
2463
|
+
this.pushSemanticDiagnostic(identifier, 'rollup function "corr" requires multiple input fields and is only valid in the select clause', `rollup:${identifier.from}`);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
if (name !== 'ewma' && name !== 'moving_average' && name !== 'percentile') {
|
|
2467
|
+
this.pushSemanticDiagnostic(body, `rollup function "${name}" does not accept arguments in the time range`, `rollup:${call.from}`);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const firstValue = args.length === 1 ? this.numericValue(args[0]) : null;
|
|
2471
|
+
if (name === 'ewma' && (firstValue === null || firstValue <= 0 || firstValue > 1)) {
|
|
2472
|
+
this.pushSemanticDiagnostic(body, 'rollup function "ewma" expects one numeric alpha argument in range (0, 1]', `rollup:${call.from}`);
|
|
2473
|
+
}
|
|
2474
|
+
if (name === 'moving_average' && (firstValue === null || !Number.isInteger(firstValue) || firstValue < 1)) {
|
|
2475
|
+
this.pushSemanticDiagnostic(body, 'rollup function "moving_average" expects one positive integer argument', `rollup:${call.from}`);
|
|
2476
|
+
}
|
|
2477
|
+
if (name === 'percentile') {
|
|
2478
|
+
if (firstValue === null || firstValue < 0 || firstValue > 100) {
|
|
2479
|
+
this.pushSemanticDiagnostic(body, 'rollup function "percentile" expects one numeric argument between 0 and 100', `rollup:${call.from}`);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
checkFunctionCall(call) {
|
|
2484
|
+
const identifier = call.getChild('FunctionIdentifier');
|
|
2485
|
+
const body = call.getChild('FunctionCallBody');
|
|
2486
|
+
if (!identifier || !body)
|
|
2487
|
+
return;
|
|
2488
|
+
const name = this.nodeText(identifier).toLowerCase();
|
|
2489
|
+
const args = this.directChildren(body, 'ArgItem');
|
|
2490
|
+
if (name === 'ewma') {
|
|
2491
|
+
const alpha = args.length === 2 ? this.numericValue(args[1]) : null;
|
|
2492
|
+
if (alpha === null || alpha <= 0 || alpha > 1) {
|
|
2493
|
+
this.pushSemanticDiagnostic(body, 'function "ewma" expects a field and an alpha argument in range (0, 1]', `function:${call.from}`);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
else if (name === 'moving_average') {
|
|
2497
|
+
const window = args.length === 2 ? this.numericValue(args[1]) : null;
|
|
2498
|
+
if (window === null || !Number.isInteger(window) || window < 1) {
|
|
2499
|
+
this.pushSemanticDiagnostic(body, 'function "moving_average" expects a field and a positive integer window', `function:${call.from}`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
else if (name === 'percentile') {
|
|
2503
|
+
const percentile = args.length === 2 ? this.numericValue(args[1]) : null;
|
|
2504
|
+
if (percentile === null || percentile < 0 || percentile > 100) {
|
|
2505
|
+
this.pushSemanticDiagnostic(body, 'function "percentile" expects a field and a numeric percentile between 0 and 100', `function:${call.from}`);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
else if (name === 'corr' && args.length !== 2) {
|
|
2509
|
+
this.pushSemanticDiagnostic(body, 'function "corr" expects exactly two input fields', `function:${call.from}`);
|
|
2510
|
+
}
|
|
2511
|
+
else if ((name === 'slope' || name === 'zscore' || name === 'mad_score' || name === 'change_score') && args.length !== 1) {
|
|
2512
|
+
this.pushSemanticDiagnostic(body, `function "${name}" expects exactly one input field`, `function:${call.from}`);
|
|
2513
|
+
}
|
|
2514
|
+
else if (/^p(?:50|75|90|95|99)$/.test(name) && args.length !== 1) {
|
|
2515
|
+
this.pushSemanticDiagnostic(body, `function "${name}" expects exactly one input field`, `function:${call.from}`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
checkOuterFunctionCall(call) {
|
|
2519
|
+
const identifier = call.firstChild;
|
|
2520
|
+
if (!identifier || identifier.name === 'FunctionIdentifier')
|
|
2521
|
+
return;
|
|
2522
|
+
const name = this.nodeText(identifier).toLowerCase();
|
|
2523
|
+
const body = identifier.nextSibling;
|
|
2524
|
+
if (!body)
|
|
2525
|
+
return;
|
|
2526
|
+
const numbers = this.directChildren(body, 'Number');
|
|
2527
|
+
const argument = numbers.length > 0 ? this.numericValue(numbers[numbers.length - 1]) : null;
|
|
2528
|
+
if (name === 'moving_average' || name === 'top' || name === 'bottom') {
|
|
2529
|
+
if (argument === null || !Number.isInteger(argument) || argument < 1) {
|
|
2530
|
+
this.pushSemanticDiagnostic(identifier, `outer function "${name}" expects a positive integer argument`, `outer-function:${call.from}`);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
else if (name === 'round' && argument !== null && (!Number.isInteger(argument) || argument < 0)) {
|
|
2534
|
+
this.pushSemanticDiagnostic(identifier, 'outer function "round" expects a non-negative integer digits argument', `outer-function:${call.from}`);
|
|
2535
|
+
}
|
|
2536
|
+
else if (name === 'dbscan' && argument !== null && (argument <= 0 || argument > 3)) {
|
|
2537
|
+
this.pushSemanticDiagnostic(identifier, 'outer function "dbscan" expects eps in range (0, 3]', `outer-function:${call.from}`);
|
|
2538
|
+
}
|
|
2539
|
+
else if (name === 'forecast' && argument !== null && (!Number.isInteger(argument) || argument < 1)) {
|
|
2540
|
+
this.pushSemanticDiagnostic(identifier, 'outer function "forecast" expects a positive integer steps argument', `outer-function:${call.from}`);
|
|
2541
|
+
}
|
|
2542
|
+
else if (name === 'fill') {
|
|
2543
|
+
const mode = this.directChildren(body, 'Identifier')[0];
|
|
2544
|
+
if (mode && !/^(linear|previous)$/i.test(this.nodeText(mode))) {
|
|
2545
|
+
this.pushSemanticDiagnostic(mode, 'outer function "fill" mode must be "LINEAR" or "PREVIOUS"', `outer-function:${call.from}`);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
checkProjectionAlias(target) {
|
|
2550
|
+
var _a;
|
|
2551
|
+
const outerAlias = target.getChild('As');
|
|
2552
|
+
const projection = target.getChild('Projection');
|
|
2553
|
+
if (!outerAlias || !projection)
|
|
2554
|
+
return;
|
|
2555
|
+
const items = this.directChildren(projection, 'ProjectionItem');
|
|
2556
|
+
if (items.length !== 1) {
|
|
2557
|
+
this.pushSemanticDiagnostic(outerAlias, 'an alias after ")" requires exactly one projection item', `projection-alias:${target.from}`);
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if ((_a = items[0].getChild('ProjectionField')) === null || _a === void 0 ? void 0 : _a.getChild('As')) {
|
|
2561
|
+
this.pushSemanticDiagnostic(outerAlias, 'projection expression cannot have both inner and outer aliases', `projection-alias:${target.from}`);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
checkFilterSubqueryOperator(subquery) {
|
|
2565
|
+
const binary = this.findAncestor(subquery.parent, 'BinaryExpr');
|
|
2566
|
+
if (!binary) {
|
|
2567
|
+
this.pushSemanticDiagnostic(subquery, 'filter subquery requires "IN" or "NOT IN"', `filter-subquery:${subquery.from}`);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
const operators = new Set([
|
|
2571
|
+
'Pow',
|
|
2572
|
+
'Mul',
|
|
2573
|
+
'Div',
|
|
2574
|
+
'Mod',
|
|
2575
|
+
'Add',
|
|
2576
|
+
'Sub',
|
|
2577
|
+
'Eql',
|
|
2578
|
+
'EqlSingle',
|
|
2579
|
+
'Neq',
|
|
2580
|
+
'Lte',
|
|
2581
|
+
'Lss',
|
|
2582
|
+
'Gte',
|
|
2583
|
+
'Gtr',
|
|
2584
|
+
'EqlRegex',
|
|
2585
|
+
'NeqRegex',
|
|
2586
|
+
'In',
|
|
2587
|
+
'NotInSeq',
|
|
2588
|
+
'And',
|
|
2589
|
+
'Or',
|
|
2590
|
+
'DAnd',
|
|
2591
|
+
'DOr',
|
|
2592
|
+
]);
|
|
2593
|
+
let operator = null;
|
|
2594
|
+
let child = binary.firstChild;
|
|
2595
|
+
while (child) {
|
|
2596
|
+
if (operators.has(child.name)) {
|
|
2597
|
+
operator = child;
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
child = child.nextSibling;
|
|
2601
|
+
}
|
|
2602
|
+
if (!operator || (operator.name !== 'In' && operator.name !== 'NotInSeq') || subquery.from < operator.to) {
|
|
2603
|
+
this.pushSemanticDiagnostic(operator !== null && operator !== void 0 ? operator : subquery, 'filter subquery requires "IN" or "NOT IN"', `filter-subquery:${subquery.from}`);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
checkFieldName(field) {
|
|
2607
|
+
if (field.getChild('JsonField'))
|
|
2608
|
+
return;
|
|
2609
|
+
const text = this.nodeText(field);
|
|
2610
|
+
if (!text || text.startsWith('`') || text.startsWith('"') || text.startsWith("'"))
|
|
2611
|
+
return;
|
|
2612
|
+
if (text.includes('-')) {
|
|
2613
|
+
this.pushSemanticDiagnostic(field, 'field names containing "-" must be enclosed in backticks', `field-name:${field.from}`);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
checkCaseFunction(identifier) {
|
|
2617
|
+
const name = this.nodeText(identifier).toLowerCase();
|
|
2618
|
+
if (name === 'uint' || name === 'bool' || name === 'substr') {
|
|
2619
|
+
this.pushSemanticDiagnostic(identifier, `function "${name}" is not supported inside CASE`, `case-function:${identifier.from}`);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
checkCaseResults(caseExpr) {
|
|
2623
|
+
let clause = caseExpr.firstChild;
|
|
2624
|
+
while (clause) {
|
|
2625
|
+
if (clause.name === 'CaseWhenClause' || clause.name === 'CaseElseClause') {
|
|
2626
|
+
const values = this.directChildren(clause, 'CaseValue');
|
|
2627
|
+
const result = values[values.length - 1];
|
|
2628
|
+
if (result && this.nodeText(result).trim().toLowerCase() === 'end') {
|
|
2629
|
+
const message = clause.name === 'CaseWhenClause' ? 'CASE result expected after "THEN"' : 'CASE result expected after "ELSE"';
|
|
2630
|
+
this.pushSemanticDiagnostic(result, message, `case-result:${result.from}`);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
clause = clause.nextSibling;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
checkShowFunctionRequirements(show) {
|
|
2637
|
+
const identifier = show.getChild('ShowIdentifier');
|
|
2638
|
+
const body = show.getChild('FunctionCallBody');
|
|
2639
|
+
if (!identifier || !body)
|
|
2640
|
+
return;
|
|
2641
|
+
const name = this.nodeText(identifier).toLowerCase();
|
|
2642
|
+
const bodyText = this.maskStringsAndLineComments(this.nodeText(body));
|
|
2643
|
+
const hasKeyin = /\bkeyin\s*=/.test(bodyText.toLowerCase());
|
|
2644
|
+
const hasFrom = /\bfrom\s*=/.test(bodyText.toLowerCase());
|
|
2645
|
+
const filter = show.getChild('FilterClause');
|
|
2646
|
+
const hasSourceFilter = Boolean(filter && /@__source__/.test(this.maskStringsAndLineComments(this.nodeText(filter))));
|
|
2647
|
+
if ((name === 'show_tag_value' || name === 'show_tag_value_cardinality' || name === 'show_series_count_by_tag_value') && !hasKeyin) {
|
|
2648
|
+
this.pushSemanticDiagnostic(identifier, `"${name}" requires the named argument "keyin"`, `show:${show.from}:keyin`);
|
|
2649
|
+
}
|
|
2650
|
+
if (name.startsWith('show_series_count_by_') && !hasFrom && !hasSourceFilter) {
|
|
2651
|
+
this.pushSemanticDiagnostic(identifier, `"${name}" requires "from" or an @__source__ filter`, `show:${show.from}:from`);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
pushSemanticDiagnostic(node, message, group) {
|
|
2655
|
+
this.pushSemanticDiagnosticAt(node.from, node.to, message, group);
|
|
2656
|
+
}
|
|
2657
|
+
pushSemanticDiagnosticAt(from, to, message, group) {
|
|
2658
|
+
const diagnostic = this.makeDiagnosticAt(from, Math.max(from + 1, to), message, group);
|
|
2659
|
+
if (!this.diagnostics.some((existing) => this.isSameDiagnostic(existing, diagnostic)))
|
|
2660
|
+
this.appendDiagnostics([diagnostic]);
|
|
2661
|
+
}
|
|
2662
|
+
directChildren(node, name) {
|
|
2663
|
+
const children = [];
|
|
2664
|
+
let child = node.firstChild;
|
|
2665
|
+
while (child) {
|
|
2666
|
+
if (child.name === name)
|
|
2667
|
+
children.push(child);
|
|
2668
|
+
child = child.nextSibling;
|
|
2669
|
+
}
|
|
2670
|
+
return children;
|
|
2671
|
+
}
|
|
2672
|
+
numericValue(node) {
|
|
2673
|
+
if (!node)
|
|
2674
|
+
return null;
|
|
2675
|
+
const number = node.name === 'Number' ? node : node.getChild('Number');
|
|
2676
|
+
if (!number)
|
|
2677
|
+
return null;
|
|
2678
|
+
const sign = node.name !== 'Number' && node.getChild('Sub') ? -1 : 1;
|
|
2679
|
+
const value = sign * Number(this.nodeText(number).replace(/_/g, ''));
|
|
2680
|
+
return Number.isFinite(value) ? value : null;
|
|
2681
|
+
}
|
|
2682
|
+
splitTopLevelSegments(text, separator) {
|
|
2683
|
+
const segments = [];
|
|
2684
|
+
let from = 0;
|
|
2685
|
+
let quote = '';
|
|
2686
|
+
let escaped = false;
|
|
2687
|
+
let depth = 0;
|
|
2688
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
2689
|
+
const char = text[index];
|
|
2690
|
+
if (quote) {
|
|
2691
|
+
if (escaped)
|
|
2692
|
+
escaped = false;
|
|
2693
|
+
else if (char === '\\' && quote !== '`')
|
|
2694
|
+
escaped = true;
|
|
2695
|
+
else if (char === quote)
|
|
2696
|
+
quote = '';
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
2700
|
+
quote = char;
|
|
2701
|
+
}
|
|
2702
|
+
else if (char === '(') {
|
|
2703
|
+
depth += 1;
|
|
2704
|
+
}
|
|
2705
|
+
else if (char === ')') {
|
|
2706
|
+
depth = Math.max(0, depth - 1);
|
|
2707
|
+
}
|
|
2708
|
+
else if (char === separator && depth === 0) {
|
|
2709
|
+
segments.push({ from, to: index });
|
|
2710
|
+
from = index + 1;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
segments.push({ from, to: text.length });
|
|
2714
|
+
return segments;
|
|
2715
|
+
}
|
|
2716
|
+
describeUnknownFunction(node) {
|
|
2717
|
+
const contextNames = new Set(['ProjectionField', 'FilterClause', 'HavingClause', 'ByExpr']);
|
|
2718
|
+
let context = node.parent;
|
|
2719
|
+
while (context && !contextNames.has(context.name))
|
|
2720
|
+
context = context.parent;
|
|
2721
|
+
if (!context)
|
|
2722
|
+
return null;
|
|
2723
|
+
const text = this.maskStringsAndLineComments(this.nodeText(context));
|
|
2724
|
+
const functionPattern = /\b([A-Za-z_][A-Za-z0-9_-]*)\s*\(/g;
|
|
2725
|
+
let match = functionPattern.exec(text);
|
|
2726
|
+
while (match) {
|
|
2727
|
+
const name = match[1];
|
|
2728
|
+
const nameFrom = context.from + match.index;
|
|
2729
|
+
if (!this.containsFunctionCallStartingAt(context, nameFrom)) {
|
|
2730
|
+
return this.makeDiagnosticAt(nameFrom, nameFrom + name.length, `unknown function "${name}"`, `unknown-function:${context.from}:${nameFrom}`);
|
|
2731
|
+
}
|
|
2732
|
+
match = functionPattern.exec(text);
|
|
2733
|
+
}
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
containsFunctionCallStartingAt(node, from) {
|
|
2737
|
+
if ((node.name === 'FunctionCall' || node.name === 'CaseFunctionCall' || node.name === 'FieldCall') && node.from === from)
|
|
2738
|
+
return true;
|
|
2739
|
+
let child = node.firstChild;
|
|
2740
|
+
while (child) {
|
|
2741
|
+
if (child.from <= from && child.to >= from && this.containsFunctionCallStartingAt(child, from))
|
|
2742
|
+
return true;
|
|
2743
|
+
child = child.nextSibling;
|
|
2744
|
+
}
|
|
2745
|
+
return false;
|
|
44
2746
|
}
|
|
45
2747
|
}
|
|
46
2748
|
//# sourceMappingURL=parser.js.map
|