Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/contextkey/common/contextkey.ts
5251 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CharCode } from '../../../base/common/charCode.js';
7
import { Event } from '../../../base/common/event.js';
8
import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from '../../../base/common/platform.js';
9
import { isFalsyOrWhitespace } from '../../../base/common/strings.js';
10
import { Scanner, LexingError, Token, TokenType } from './scanner.js';
11
import { createDecorator } from '../../instantiation/common/instantiation.js';
12
import { localize } from '../../../nls.js';
13
import { IDisposable } from '../../../base/common/lifecycle.js';
14
import { illegalArgument } from '../../../base/common/errors.js';
15
16
const CONSTANT_VALUES = new Map<string, boolean>();
17
CONSTANT_VALUES.set('false', false);
18
CONSTANT_VALUES.set('true', true);
19
CONSTANT_VALUES.set('isMac', isMacintosh);
20
CONSTANT_VALUES.set('isLinux', isLinux);
21
CONSTANT_VALUES.set('isWindows', isWindows);
22
CONSTANT_VALUES.set('isWeb', isWeb);
23
CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb);
24
CONSTANT_VALUES.set('isEdge', isEdge);
25
CONSTANT_VALUES.set('isFirefox', isFirefox);
26
CONSTANT_VALUES.set('isChrome', isChrome);
27
CONSTANT_VALUES.set('isSafari', isSafari);
28
29
/** allow register constant context keys that are known only after startup; requires running `substituteConstants` on the context key - https://github.com/microsoft/vscode/issues/174218#issuecomment-1437972127 */
30
export function setConstant(key: string, value: boolean) {
31
if (CONSTANT_VALUES.get(key) !== undefined) { throw illegalArgument('contextkey.setConstant(k, v) invoked with already set constant `k`'); }
32
33
CONSTANT_VALUES.set(key, value);
34
}
35
36
const hasOwnProperty = Object.prototype.hasOwnProperty;
37
38
export const enum ContextKeyExprType {
39
False = 0,
40
True = 1,
41
Defined = 2,
42
Not = 3,
43
Equals = 4,
44
NotEquals = 5,
45
And = 6,
46
Regex = 7,
47
NotRegex = 8,
48
Or = 9,
49
In = 10,
50
NotIn = 11,
51
Greater = 12,
52
GreaterEquals = 13,
53
Smaller = 14,
54
SmallerEquals = 15,
55
}
56
57
export interface IContextKeyExprMapper {
58
mapDefined(key: string): ContextKeyExpression;
59
mapNot(key: string): ContextKeyExpression;
60
mapEquals(key: string, value: any): ContextKeyExpression;
61
mapNotEquals(key: string, value: any): ContextKeyExpression;
62
mapGreater(key: string, value: any): ContextKeyExpression;
63
mapGreaterEquals(key: string, value: any): ContextKeyExpression;
64
mapSmaller(key: string, value: any): ContextKeyExpression;
65
mapSmallerEquals(key: string, value: any): ContextKeyExpression;
66
mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr;
67
mapIn(key: string, valueKey: string): ContextKeyInExpr;
68
mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr;
69
}
70
71
export interface IContextKeyExpression {
72
cmp(other: ContextKeyExpression): number;
73
equals(other: ContextKeyExpression): boolean;
74
substituteConstants(): ContextKeyExpression | undefined;
75
evaluate(context: IContext): boolean;
76
serialize(): string;
77
keys(): string[];
78
map(mapFnc: IContextKeyExprMapper): ContextKeyExpression;
79
negate(): ContextKeyExpression;
80
81
}
82
83
export type ContextKeyExpression = (
84
ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr
85
| ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr
86
| ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr
87
| ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr
88
| ContextKeySmallerExpr | ContextKeySmallerEqualsExpr
89
);
90
91
92
/*
93
94
Syntax grammar:
95
96
```ebnf
97
98
expression ::= or
99
100
or ::= and { '||' and }*
101
102
and ::= term { '&&' term }*
103
104
term ::=
105
| '!' (KEY | true | false | parenthesized)
106
| primary
107
108
primary ::=
109
| 'true'
110
| 'false'
111
| parenthesized
112
| KEY '=~' REGEX
113
| KEY [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'not' 'in' | 'in') value ]
114
115
parenthesized ::=
116
| '(' expression ')'
117
118
value ::=
119
| 'true'
120
| 'false'
121
| 'in' // we support `in` as a value because there's an extension that uses it, ie "when": "languageId == in"
122
| VALUE // matched by the same regex as KEY; consider putting the value in single quotes if it's a string (e.g., with spaces)
123
| SINGLE_QUOTED_STR
124
| EMPTY_STR // this allows "when": "foo == " which's used by existing extensions
125
126
```
127
*/
128
129
export type ParserConfig = {
130
/**
131
* with this option enabled, the parser can recover from regex parsing errors, e.g., unescaped slashes: `/src//` is accepted as `/src\//` would be
132
*/
133
regexParsingWithErrorRecovery: boolean;
134
};
135
136
const defaultConfig: ParserConfig = {
137
regexParsingWithErrorRecovery: true
138
};
139
140
export type ParsingError = {
141
message: string;
142
offset: number;
143
lexeme: string;
144
additionalInfo?: string;
145
};
146
147
const errorEmptyString = localize('contextkey.parser.error.emptyString', "Empty context key expression");
148
const hintEmptyString = localize('contextkey.parser.error.emptyString.hint', "Did you forget to write an expression? You can also put 'false' or 'true' to always evaluate to false or true, respectively.");
149
const errorNoInAfterNot = localize('contextkey.parser.error.noInAfterNot', "'in' after 'not'.");
150
const errorClosingParenthesis = localize('contextkey.parser.error.closingParenthesis', "closing parenthesis ')'");
151
const errorUnexpectedToken = localize('contextkey.parser.error.unexpectedToken', "Unexpected token");
152
const hintUnexpectedToken = localize('contextkey.parser.error.unexpectedToken.hint', "Did you forget to put && or || before the token?");
153
const errorUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF', "Unexpected end of expression");
154
const hintUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF.hint', "Did you forget to put a context key?");
155
156
/**
157
* A parser for context key expressions.
158
*
159
* Example:
160
* ```ts
161
* const parser = new Parser();
162
* const expr = parser.parse('foo == "bar" && baz == true');
163
*
164
* if (expr === undefined) {
165
* // there were lexing or parsing errors
166
* // process lexing errors with `parser.lexingErrors`
167
* // process parsing errors with `parser.parsingErrors`
168
* } else {
169
* // expr is a valid expression
170
* }
171
* ```
172
*/
173
export class Parser {
174
// Note: this doesn't produce an exact syntax tree but a normalized one
175
// ContextKeyExpression's that we use as AST nodes do not expose constructors that do not normalize
176
177
private static _parseError = new Error();
178
179
// lifetime note: `_scanner` lives as long as the parser does, i.e., is not reset between calls to `parse`
180
private readonly _scanner = new Scanner();
181
182
// lifetime note: `_tokens`, `_current`, and `_parsingErrors` must be reset between calls to `parse`
183
private _tokens: Token[] = [];
184
private _current = 0; // invariant: 0 <= this._current < this._tokens.length ; any incrementation of this value must first call `_isAtEnd`
185
private _parsingErrors: ParsingError[] = [];
186
187
get lexingErrors(): Readonly<LexingError[]> {
188
return this._scanner.errors;
189
}
190
191
get parsingErrors(): Readonly<ParsingError[]> {
192
return this._parsingErrors;
193
}
194
195
constructor(private readonly _config: ParserConfig = defaultConfig) {
196
}
197
198
/**
199
* Parse a context key expression.
200
*
201
* @param input the expression to parse
202
* @returns the parsed expression or `undefined` if there's an error - call `lexingErrors` and `parsingErrors` to see the errors
203
*/
204
parse(input: string): ContextKeyExpression | undefined {
205
206
if (input === '') {
207
this._parsingErrors.push({ message: errorEmptyString, offset: 0, lexeme: '', additionalInfo: hintEmptyString });
208
return undefined;
209
}
210
211
this._tokens = this._scanner.reset(input).scan();
212
// @ulugbekna: we do not stop parsing if there are lexing errors to be able to reconstruct regexes with unescaped slashes; TODO@ulugbekna: make this respect config option for recovery
213
214
this._current = 0;
215
this._parsingErrors = [];
216
217
try {
218
const expr = this._expr();
219
if (!this._isAtEnd()) {
220
const peek = this._peek();
221
const additionalInfo = peek.type === TokenType.Str ? hintUnexpectedToken : undefined;
222
this._parsingErrors.push({ message: errorUnexpectedToken, offset: peek.offset, lexeme: Scanner.getLexeme(peek), additionalInfo });
223
throw Parser._parseError;
224
}
225
return expr;
226
} catch (e) {
227
if (!(e === Parser._parseError)) {
228
throw e;
229
}
230
return undefined;
231
}
232
}
233
234
private _expr(): ContextKeyExpression | undefined {
235
return this._or();
236
}
237
238
private _or(): ContextKeyExpression | undefined {
239
const expr = [this._and()];
240
241
while (this._matchOne(TokenType.Or)) {
242
const right = this._and();
243
expr.push(right);
244
}
245
246
return expr.length === 1 ? expr[0] : ContextKeyExpr.or(...expr);
247
}
248
249
private _and(): ContextKeyExpression | undefined {
250
const expr = [this._term()];
251
252
while (this._matchOne(TokenType.And)) {
253
const right = this._term();
254
expr.push(right);
255
}
256
257
return expr.length === 1 ? expr[0] : ContextKeyExpr.and(...expr);
258
}
259
260
private _term(): ContextKeyExpression | undefined {
261
if (this._matchOne(TokenType.Neg)) {
262
const peek = this._peek();
263
switch (peek.type) {
264
case TokenType.True:
265
this._advance();
266
return ContextKeyFalseExpr.INSTANCE;
267
case TokenType.False:
268
this._advance();
269
return ContextKeyTrueExpr.INSTANCE;
270
case TokenType.LParen: {
271
this._advance();
272
const expr = this._expr();
273
this._consume(TokenType.RParen, errorClosingParenthesis);
274
return expr?.negate();
275
}
276
case TokenType.Str:
277
this._advance();
278
return ContextKeyNotExpr.create(peek.lexeme);
279
default:
280
throw this._errExpectedButGot(`KEY | true | false | '(' expression ')'`, peek);
281
}
282
}
283
return this._primary();
284
}
285
286
private _primary(): ContextKeyExpression | undefined {
287
288
const peek = this._peek();
289
switch (peek.type) {
290
case TokenType.True:
291
this._advance();
292
return ContextKeyExpr.true();
293
294
case TokenType.False:
295
this._advance();
296
return ContextKeyExpr.false();
297
298
case TokenType.LParen: {
299
this._advance();
300
const expr = this._expr();
301
this._consume(TokenType.RParen, errorClosingParenthesis);
302
return expr;
303
}
304
305
case TokenType.Str: {
306
// KEY
307
const key = peek.lexeme;
308
this._advance();
309
310
// =~ regex
311
if (this._matchOne(TokenType.RegexOp)) {
312
313
// @ulugbekna: we need to reconstruct the regex from the tokens because some extensions use unescaped slashes in regexes
314
const expr = this._peek();
315
316
if (!this._config.regexParsingWithErrorRecovery) {
317
this._advance();
318
if (expr.type !== TokenType.RegexStr) {
319
throw this._errExpectedButGot(`REGEX`, expr);
320
}
321
const regexLexeme = expr.lexeme;
322
const closingSlashIndex = regexLexeme.lastIndexOf('/');
323
const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));
324
let regexp: RegExp | null;
325
try {
326
regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);
327
} catch (e) {
328
throw this._errExpectedButGot(`REGEX`, expr);
329
}
330
return ContextKeyRegexExpr.create(key, regexp);
331
}
332
333
switch (expr.type) {
334
case TokenType.RegexStr:
335
case TokenType.Error: { // also handle an ErrorToken in case of smth such as /(/file)/
336
const lexemeReconstruction = [expr.lexeme]; // /REGEX/ or /REGEX/FLAGS
337
this._advance();
338
339
let followingToken = this._peek();
340
let parenBalance = 0;
341
for (let i = 0; i < expr.lexeme.length; i++) {
342
if (expr.lexeme.charCodeAt(i) === CharCode.OpenParen) {
343
parenBalance++;
344
} else if (expr.lexeme.charCodeAt(i) === CharCode.CloseParen) {
345
parenBalance--;
346
}
347
}
348
349
while (!this._isAtEnd() && followingToken.type !== TokenType.And && followingToken.type !== TokenType.Or) {
350
switch (followingToken.type) {
351
case TokenType.LParen:
352
parenBalance++;
353
break;
354
case TokenType.RParen:
355
parenBalance--;
356
break;
357
case TokenType.RegexStr:
358
case TokenType.QuotedStr:
359
for (let i = 0; i < followingToken.lexeme.length; i++) {
360
if (followingToken.lexeme.charCodeAt(i) === CharCode.OpenParen) {
361
parenBalance++;
362
} else if (expr.lexeme.charCodeAt(i) === CharCode.CloseParen) {
363
parenBalance--;
364
}
365
}
366
}
367
if (parenBalance < 0) {
368
break;
369
}
370
lexemeReconstruction.push(Scanner.getLexeme(followingToken));
371
this._advance();
372
followingToken = this._peek();
373
}
374
375
const regexLexeme = lexemeReconstruction.join('');
376
const closingSlashIndex = regexLexeme.lastIndexOf('/');
377
const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1));
378
let regexp: RegExp | null;
379
try {
380
regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags);
381
} catch (e) {
382
throw this._errExpectedButGot(`REGEX`, expr);
383
}
384
return ContextKeyExpr.regex(key, regexp);
385
}
386
387
case TokenType.QuotedStr: {
388
const serializedValue = expr.lexeme;
389
this._advance();
390
// replicate old regex parsing behavior
391
392
let regex: RegExp | null = null;
393
394
if (!isFalsyOrWhitespace(serializedValue)) {
395
const start = serializedValue.indexOf('/');
396
const end = serializedValue.lastIndexOf('/');
397
if (start !== end && start >= 0) {
398
399
const value = serializedValue.slice(start + 1, end);
400
const caseIgnoreFlag = serializedValue[end + 1] === 'i' ? 'i' : '';
401
try {
402
regex = new RegExp(value, caseIgnoreFlag);
403
} catch (_e) {
404
throw this._errExpectedButGot(`REGEX`, expr);
405
}
406
}
407
}
408
409
if (regex === null) {
410
throw this._errExpectedButGot('REGEX', expr);
411
}
412
413
return ContextKeyRegexExpr.create(key, regex);
414
}
415
416
default:
417
throw this._errExpectedButGot('REGEX', this._peek());
418
}
419
}
420
421
// [ 'not' 'in' value ]
422
if (this._matchOne(TokenType.Not)) {
423
this._consume(TokenType.In, errorNoInAfterNot);
424
const right = this._value();
425
return ContextKeyExpr.notIn(key, right);
426
}
427
428
// [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in') value ]
429
const maybeOp = this._peek().type;
430
switch (maybeOp) {
431
case TokenType.Eq: {
432
this._advance();
433
434
const right = this._value();
435
if (this._previous().type === TokenType.QuotedStr) { // to preserve old parser behavior: "foo == 'true'" is preserved as "foo == 'true'", but "foo == true" is optimized as "foo"
436
return ContextKeyExpr.equals(key, right);
437
}
438
switch (right) {
439
case 'true':
440
return ContextKeyExpr.has(key);
441
case 'false':
442
return ContextKeyExpr.not(key);
443
default:
444
return ContextKeyExpr.equals(key, right);
445
}
446
}
447
448
case TokenType.NotEq: {
449
this._advance();
450
451
const right = this._value();
452
if (this._previous().type === TokenType.QuotedStr) { // same as above with "foo != 'true'"
453
return ContextKeyExpr.notEquals(key, right);
454
}
455
switch (right) {
456
case 'true':
457
return ContextKeyExpr.not(key);
458
case 'false':
459
return ContextKeyExpr.has(key);
460
default:
461
return ContextKeyExpr.notEquals(key, right);
462
}
463
}
464
// TODO: ContextKeyExpr.smaller(key, right) accepts only `number` as `right` AND during eval of this node, we just eval to `false` if `right` is not a number
465
// consequently, package.json linter should _warn_ the user if they're passing undesired things to ops
466
case TokenType.Lt:
467
this._advance();
468
return ContextKeySmallerExpr.create(key, this._value());
469
470
case TokenType.LtEq:
471
this._advance();
472
return ContextKeySmallerEqualsExpr.create(key, this._value());
473
474
case TokenType.Gt:
475
this._advance();
476
return ContextKeyGreaterExpr.create(key, this._value());
477
478
case TokenType.GtEq:
479
this._advance();
480
return ContextKeyGreaterEqualsExpr.create(key, this._value());
481
482
case TokenType.In:
483
this._advance();
484
return ContextKeyExpr.in(key, this._value());
485
486
default:
487
return ContextKeyExpr.has(key);
488
}
489
}
490
491
case TokenType.EOF:
492
this._parsingErrors.push({ message: errorUnexpectedEOF, offset: peek.offset, lexeme: '', additionalInfo: hintUnexpectedEOF });
493
throw Parser._parseError;
494
495
default:
496
throw this._errExpectedButGot(`true | false | KEY \n\t| KEY '=~' REGEX \n\t| KEY ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not' 'in') value`, this._peek());
497
498
}
499
}
500
501
private _value(): string {
502
const token = this._peek();
503
switch (token.type) {
504
case TokenType.Str:
505
case TokenType.QuotedStr:
506
this._advance();
507
return token.lexeme;
508
case TokenType.True:
509
this._advance();
510
return 'true';
511
case TokenType.False:
512
this._advance();
513
return 'false';
514
case TokenType.In: // we support `in` as a value, e.g., "when": "languageId == in" - exists in existing extensions
515
this._advance();
516
return 'in';
517
default:
518
// this allows "when": "foo == " which's used by existing extensions
519
// we do not call `_advance` on purpose - we don't want to eat unintended tokens
520
return '';
521
}
522
}
523
524
private _flagsGYRe = /g|y/g;
525
private _removeFlagsGY(flags: string): string {
526
return flags.replaceAll(this._flagsGYRe, '');
527
}
528
529
// careful: this can throw if current token is the initial one (ie index = 0)
530
private _previous() {
531
return this._tokens[this._current - 1];
532
}
533
534
private _matchOne(token: TokenType) {
535
if (this._check(token)) {
536
this._advance();
537
return true;
538
}
539
540
return false;
541
}
542
543
private _advance() {
544
if (!this._isAtEnd()) {
545
this._current++;
546
}
547
return this._previous();
548
}
549
550
private _consume(type: TokenType, message: string) {
551
if (this._check(type)) {
552
return this._advance();
553
}
554
555
throw this._errExpectedButGot(message, this._peek());
556
}
557
558
private _errExpectedButGot(expected: string, got: Token, additionalInfo?: string) {
559
const message = localize('contextkey.parser.error.expectedButGot', "Expected: {0}\nReceived: '{1}'.", expected, Scanner.getLexeme(got));
560
const offset = got.offset;
561
const lexeme = Scanner.getLexeme(got);
562
this._parsingErrors.push({ message, offset, lexeme, additionalInfo });
563
return Parser._parseError;
564
}
565
566
private _check(type: TokenType) {
567
return this._peek().type === type;
568
}
569
570
private _peek() {
571
return this._tokens[this._current];
572
}
573
574
private _isAtEnd() {
575
return this._peek().type === TokenType.EOF;
576
}
577
}
578
579
export abstract class ContextKeyExpr {
580
581
public static false(): ContextKeyExpression {
582
return ContextKeyFalseExpr.INSTANCE;
583
}
584
public static true(): ContextKeyExpression {
585
return ContextKeyTrueExpr.INSTANCE;
586
}
587
public static has(key: string): ContextKeyExpression {
588
return ContextKeyDefinedExpr.create(key);
589
}
590
public static equals(key: string, value: any): ContextKeyExpression {
591
return ContextKeyEqualsExpr.create(key, value);
592
}
593
public static notEquals(key: string, value: any): ContextKeyExpression {
594
return ContextKeyNotEqualsExpr.create(key, value);
595
}
596
public static regex(key: string, value: RegExp): ContextKeyExpression {
597
return ContextKeyRegexExpr.create(key, value);
598
}
599
public static in(key: string, value: string): ContextKeyExpression {
600
return ContextKeyInExpr.create(key, value);
601
}
602
public static notIn(key: string, value: string): ContextKeyExpression {
603
return ContextKeyNotInExpr.create(key, value);
604
}
605
public static not(key: string): ContextKeyExpression {
606
return ContextKeyNotExpr.create(key);
607
}
608
public static and(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined {
609
return ContextKeyAndExpr.create(expr, null, true);
610
}
611
public static or(...expr: Array<ContextKeyExpression | undefined | null>): ContextKeyExpression | undefined {
612
return ContextKeyOrExpr.create(expr, null, true);
613
}
614
public static greater(key: string, value: number): ContextKeyExpression {
615
return ContextKeyGreaterExpr.create(key, value);
616
}
617
public static greaterEquals(key: string, value: number): ContextKeyExpression {
618
return ContextKeyGreaterEqualsExpr.create(key, value);
619
}
620
public static smaller(key: string, value: number): ContextKeyExpression {
621
return ContextKeySmallerExpr.create(key, value);
622
}
623
public static smallerEquals(key: string, value: number): ContextKeyExpression {
624
return ContextKeySmallerEqualsExpr.create(key, value);
625
}
626
627
private static _parser = new Parser({ regexParsingWithErrorRecovery: false });
628
public static deserialize(serialized: string | null | undefined): ContextKeyExpression | undefined {
629
if (serialized === undefined || serialized === null) { // an empty string needs to be handled by the parser to get a corresponding parsing error reported
630
return undefined;
631
}
632
633
const expr = this._parser.parse(serialized);
634
return expr;
635
}
636
637
}
638
639
640
export function validateWhenClauses(whenClauses: string[]): any {
641
642
const parser = new Parser({ regexParsingWithErrorRecovery: false }); // we run with no recovery to guide users to use correct regexes
643
644
return whenClauses.map(whenClause => {
645
parser.parse(whenClause);
646
647
if (parser.lexingErrors.length > 0) {
648
return parser.lexingErrors.map((se: LexingError) => ({
649
errorMessage: se.additionalInfo ?
650
localize('contextkey.scanner.errorForLinterWithHint', "Unexpected token. Hint: {0}", se.additionalInfo) :
651
localize('contextkey.scanner.errorForLinter', "Unexpected token."),
652
offset: se.offset,
653
length: se.lexeme.length,
654
}));
655
} else if (parser.parsingErrors.length > 0) {
656
return parser.parsingErrors.map((pe: ParsingError) => ({
657
errorMessage: pe.additionalInfo ? `${pe.message}. ${pe.additionalInfo}` : pe.message,
658
offset: pe.offset,
659
length: pe.lexeme.length,
660
}));
661
} else {
662
return [];
663
}
664
});
665
}
666
667
export function expressionsAreEqualWithConstantSubstitution(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean {
668
const aExpr = a ? a.substituteConstants() : undefined;
669
const bExpr = b ? b.substituteConstants() : undefined;
670
if (!aExpr && !bExpr) {
671
return true;
672
}
673
if (!aExpr || !bExpr) {
674
return false;
675
}
676
return aExpr.equals(bExpr);
677
}
678
679
function cmp(a: ContextKeyExpression, b: ContextKeyExpression): number {
680
return a.cmp(b);
681
}
682
683
export class ContextKeyFalseExpr implements IContextKeyExpression {
684
public static INSTANCE = new ContextKeyFalseExpr();
685
686
public readonly type = ContextKeyExprType.False;
687
688
protected constructor() {
689
}
690
691
public cmp(other: ContextKeyExpression): number {
692
return this.type - other.type;
693
}
694
695
public equals(other: ContextKeyExpression): boolean {
696
return (other.type === this.type);
697
}
698
699
public substituteConstants(): ContextKeyExpression | undefined {
700
return this;
701
}
702
703
public evaluate(context: IContext): boolean {
704
return false;
705
}
706
707
public serialize(): string {
708
return 'false';
709
}
710
711
public keys(): string[] {
712
return [];
713
}
714
715
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
716
return this;
717
}
718
719
public negate(): ContextKeyExpression {
720
return ContextKeyTrueExpr.INSTANCE;
721
}
722
}
723
724
export class ContextKeyTrueExpr implements IContextKeyExpression {
725
public static INSTANCE = new ContextKeyTrueExpr();
726
727
public readonly type = ContextKeyExprType.True;
728
729
protected constructor() {
730
}
731
732
public cmp(other: ContextKeyExpression): number {
733
return this.type - other.type;
734
}
735
736
public equals(other: ContextKeyExpression): boolean {
737
return (other.type === this.type);
738
}
739
740
public substituteConstants(): ContextKeyExpression | undefined {
741
return this;
742
}
743
744
public evaluate(context: IContext): boolean {
745
return true;
746
}
747
748
public serialize(): string {
749
return 'true';
750
}
751
752
public keys(): string[] {
753
return [];
754
}
755
756
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
757
return this;
758
}
759
760
public negate(): ContextKeyExpression {
761
return ContextKeyFalseExpr.INSTANCE;
762
}
763
}
764
765
export class ContextKeyDefinedExpr implements IContextKeyExpression {
766
public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {
767
const constantValue = CONSTANT_VALUES.get(key);
768
if (typeof constantValue === 'boolean') {
769
return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;
770
}
771
return new ContextKeyDefinedExpr(key, negated);
772
}
773
774
public readonly type = ContextKeyExprType.Defined;
775
776
protected constructor(
777
readonly key: string,
778
private negated: ContextKeyExpression | null
779
) {
780
}
781
782
public cmp(other: ContextKeyExpression): number {
783
if (other.type !== this.type) {
784
return this.type - other.type;
785
}
786
return cmp1(this.key, other.key);
787
}
788
789
public equals(other: ContextKeyExpression): boolean {
790
if (other.type === this.type) {
791
return (this.key === other.key);
792
}
793
return false;
794
}
795
796
public substituteConstants(): ContextKeyExpression | undefined {
797
const constantValue = CONSTANT_VALUES.get(this.key);
798
if (typeof constantValue === 'boolean') {
799
return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE;
800
}
801
return this;
802
}
803
804
public evaluate(context: IContext): boolean {
805
return (!!context.getValue(this.key));
806
}
807
808
public serialize(): string {
809
return this.key;
810
}
811
812
public keys(): string[] {
813
return [this.key];
814
}
815
816
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
817
return mapFnc.mapDefined(this.key);
818
}
819
820
public negate(): ContextKeyExpression {
821
if (!this.negated) {
822
this.negated = ContextKeyNotExpr.create(this.key, this);
823
}
824
return this.negated;
825
}
826
}
827
828
export class ContextKeyEqualsExpr implements IContextKeyExpression {
829
830
public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
831
if (typeof value === 'boolean') {
832
return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated));
833
}
834
const constantValue = CONSTANT_VALUES.get(key);
835
if (typeof constantValue === 'boolean') {
836
const trueValue = constantValue ? 'true' : 'false';
837
return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
838
}
839
return new ContextKeyEqualsExpr(key, value, negated);
840
}
841
842
public readonly type = ContextKeyExprType.Equals;
843
844
private constructor(
845
private readonly key: string,
846
private readonly value: any,
847
private negated: ContextKeyExpression | null
848
) {
849
}
850
851
public cmp(other: ContextKeyExpression): number {
852
if (other.type !== this.type) {
853
return this.type - other.type;
854
}
855
return cmp2(this.key, this.value, other.key, other.value);
856
}
857
858
public equals(other: ContextKeyExpression): boolean {
859
if (other.type === this.type) {
860
return (this.key === other.key && this.value === other.value);
861
}
862
return false;
863
}
864
865
public substituteConstants(): ContextKeyExpression | undefined {
866
const constantValue = CONSTANT_VALUES.get(this.key);
867
if (typeof constantValue === 'boolean') {
868
const trueValue = constantValue ? 'true' : 'false';
869
return (this.value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE);
870
}
871
return this;
872
}
873
874
public evaluate(context: IContext): boolean {
875
// Intentional ==
876
// eslint-disable-next-line eqeqeq
877
return (context.getValue(this.key) == this.value);
878
}
879
880
public serialize(): string {
881
return `${this.key} == '${this.value}'`;
882
}
883
884
public keys(): string[] {
885
return [this.key];
886
}
887
888
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
889
return mapFnc.mapEquals(this.key, this.value);
890
}
891
892
public negate(): ContextKeyExpression {
893
if (!this.negated) {
894
this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this);
895
}
896
return this.negated;
897
}
898
}
899
900
export class ContextKeyInExpr implements IContextKeyExpression {
901
902
public static create(key: string, valueKey: string): ContextKeyInExpr {
903
return new ContextKeyInExpr(key, valueKey);
904
}
905
906
public readonly type = ContextKeyExprType.In;
907
private negated: ContextKeyExpression | null = null;
908
909
private constructor(
910
private readonly key: string,
911
private readonly valueKey: string,
912
) {
913
}
914
915
public cmp(other: ContextKeyExpression): number {
916
if (other.type !== this.type) {
917
return this.type - other.type;
918
}
919
return cmp2(this.key, this.valueKey, other.key, other.valueKey);
920
}
921
922
public equals(other: ContextKeyExpression): boolean {
923
if (other.type === this.type) {
924
return (this.key === other.key && this.valueKey === other.valueKey);
925
}
926
return false;
927
}
928
929
public substituteConstants(): ContextKeyExpression | undefined {
930
return this;
931
}
932
933
public evaluate(context: IContext): boolean {
934
const source = context.getValue(this.valueKey);
935
936
const item = context.getValue(this.key);
937
938
if (Array.isArray(source)) {
939
// eslint-disable-next-line local/code-no-any-casts
940
return source.includes(item as any);
941
}
942
943
if (typeof item === 'string' && typeof source === 'object' && source !== null) {
944
return hasOwnProperty.call(source, item);
945
}
946
return false;
947
}
948
949
public serialize(): string {
950
return `${this.key} in '${this.valueKey}'`;
951
}
952
953
public keys(): string[] {
954
return [this.key, this.valueKey];
955
}
956
957
public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr {
958
return mapFnc.mapIn(this.key, this.valueKey);
959
}
960
961
public negate(): ContextKeyExpression {
962
if (!this.negated) {
963
this.negated = ContextKeyNotInExpr.create(this.key, this.valueKey);
964
}
965
return this.negated;
966
}
967
}
968
969
export class ContextKeyNotInExpr implements IContextKeyExpression {
970
971
public static create(key: string, valueKey: string): ContextKeyNotInExpr {
972
return new ContextKeyNotInExpr(key, valueKey);
973
}
974
975
public readonly type = ContextKeyExprType.NotIn;
976
977
private readonly _negated: ContextKeyInExpr;
978
979
private constructor(
980
private readonly key: string,
981
private readonly valueKey: string,
982
) {
983
this._negated = ContextKeyInExpr.create(key, valueKey);
984
}
985
986
public cmp(other: ContextKeyExpression): number {
987
if (other.type !== this.type) {
988
return this.type - other.type;
989
}
990
return this._negated.cmp(other._negated);
991
}
992
993
public equals(other: ContextKeyExpression): boolean {
994
if (other.type === this.type) {
995
return this._negated.equals(other._negated);
996
}
997
return false;
998
}
999
1000
public substituteConstants(): ContextKeyExpression | undefined {
1001
return this;
1002
}
1003
1004
public evaluate(context: IContext): boolean {
1005
return !this._negated.evaluate(context);
1006
}
1007
1008
public serialize(): string {
1009
return `${this.key} not in '${this.valueKey}'`;
1010
}
1011
1012
public keys(): string[] {
1013
return this._negated.keys();
1014
}
1015
1016
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1017
return mapFnc.mapNotIn(this.key, this.valueKey);
1018
}
1019
1020
public negate(): ContextKeyExpression {
1021
return this._negated;
1022
}
1023
}
1024
1025
export class ContextKeyNotEqualsExpr implements IContextKeyExpression {
1026
1027
public static create(key: string, value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1028
if (typeof value === 'boolean') {
1029
if (value) {
1030
return ContextKeyNotExpr.create(key, negated);
1031
}
1032
return ContextKeyDefinedExpr.create(key, negated);
1033
}
1034
const constantValue = CONSTANT_VALUES.get(key);
1035
if (typeof constantValue === 'boolean') {
1036
const falseValue = constantValue ? 'true' : 'false';
1037
return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1038
}
1039
return new ContextKeyNotEqualsExpr(key, value, negated);
1040
}
1041
1042
public readonly type = ContextKeyExprType.NotEquals;
1043
1044
private constructor(
1045
private readonly key: string,
1046
private readonly value: any,
1047
private negated: ContextKeyExpression | null
1048
) {
1049
}
1050
1051
public cmp(other: ContextKeyExpression): number {
1052
if (other.type !== this.type) {
1053
return this.type - other.type;
1054
}
1055
return cmp2(this.key, this.value, other.key, other.value);
1056
}
1057
1058
public equals(other: ContextKeyExpression): boolean {
1059
if (other.type === this.type) {
1060
return (this.key === other.key && this.value === other.value);
1061
}
1062
return false;
1063
}
1064
1065
public substituteConstants(): ContextKeyExpression | undefined {
1066
const constantValue = CONSTANT_VALUES.get(this.key);
1067
if (typeof constantValue === 'boolean') {
1068
const falseValue = constantValue ? 'true' : 'false';
1069
return (this.value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1070
}
1071
return this;
1072
}
1073
1074
public evaluate(context: IContext): boolean {
1075
// Intentional !=
1076
// eslint-disable-next-line eqeqeq
1077
return (context.getValue(this.key) != this.value);
1078
}
1079
1080
public serialize(): string {
1081
return `${this.key} != '${this.value}'`;
1082
}
1083
1084
public keys(): string[] {
1085
return [this.key];
1086
}
1087
1088
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1089
return mapFnc.mapNotEquals(this.key, this.value);
1090
}
1091
1092
public negate(): ContextKeyExpression {
1093
if (!this.negated) {
1094
this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this);
1095
}
1096
return this.negated;
1097
}
1098
}
1099
1100
export class ContextKeyNotExpr implements IContextKeyExpression {
1101
1102
public static create(key: string, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1103
const constantValue = CONSTANT_VALUES.get(key);
1104
if (typeof constantValue === 'boolean') {
1105
return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1106
}
1107
return new ContextKeyNotExpr(key, negated);
1108
}
1109
1110
public readonly type = ContextKeyExprType.Not;
1111
1112
private constructor(
1113
private readonly key: string,
1114
private negated: ContextKeyExpression | null
1115
) {
1116
}
1117
1118
public cmp(other: ContextKeyExpression): number {
1119
if (other.type !== this.type) {
1120
return this.type - other.type;
1121
}
1122
return cmp1(this.key, other.key);
1123
}
1124
1125
public equals(other: ContextKeyExpression): boolean {
1126
if (other.type === this.type) {
1127
return (this.key === other.key);
1128
}
1129
return false;
1130
}
1131
1132
public substituteConstants(): ContextKeyExpression | undefined {
1133
const constantValue = CONSTANT_VALUES.get(this.key);
1134
if (typeof constantValue === 'boolean') {
1135
return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE);
1136
}
1137
return this;
1138
}
1139
1140
public evaluate(context: IContext): boolean {
1141
return (!context.getValue(this.key));
1142
}
1143
1144
public serialize(): string {
1145
return `!${this.key}`;
1146
}
1147
1148
public keys(): string[] {
1149
return [this.key];
1150
}
1151
1152
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1153
return mapFnc.mapNot(this.key);
1154
}
1155
1156
public negate(): ContextKeyExpression {
1157
if (!this.negated) {
1158
this.negated = ContextKeyDefinedExpr.create(this.key, this);
1159
}
1160
return this.negated;
1161
}
1162
}
1163
1164
function withFloatOrStr<T extends ContextKeyExpression>(value: any, callback: (value: number | string) => T): T | ContextKeyFalseExpr {
1165
if (typeof value === 'string') {
1166
const n = parseFloat(value);
1167
if (!isNaN(n)) {
1168
value = n;
1169
}
1170
}
1171
if (typeof value === 'string' || typeof value === 'number') {
1172
return callback(value);
1173
}
1174
return ContextKeyFalseExpr.INSTANCE;
1175
}
1176
1177
export class ContextKeyGreaterExpr implements IContextKeyExpression {
1178
1179
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1180
return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated));
1181
}
1182
1183
public readonly type = ContextKeyExprType.Greater;
1184
1185
private constructor(
1186
private readonly key: string,
1187
private readonly value: number | string,
1188
private negated: ContextKeyExpression | null
1189
) { }
1190
1191
public cmp(other: ContextKeyExpression): number {
1192
if (other.type !== this.type) {
1193
return this.type - other.type;
1194
}
1195
return cmp2(this.key, this.value, other.key, other.value);
1196
}
1197
1198
public equals(other: ContextKeyExpression): boolean {
1199
if (other.type === this.type) {
1200
return (this.key === other.key && this.value === other.value);
1201
}
1202
return false;
1203
}
1204
1205
public substituteConstants(): ContextKeyExpression | undefined {
1206
return this;
1207
}
1208
1209
public evaluate(context: IContext): boolean {
1210
if (typeof this.value === 'string') {
1211
return false;
1212
}
1213
return (parseFloat(context.getValue<any>(this.key)) > this.value);
1214
}
1215
1216
public serialize(): string {
1217
return `${this.key} > ${this.value}`;
1218
}
1219
1220
public keys(): string[] {
1221
return [this.key];
1222
}
1223
1224
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1225
return mapFnc.mapGreater(this.key, this.value);
1226
}
1227
1228
public negate(): ContextKeyExpression {
1229
if (!this.negated) {
1230
this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this);
1231
}
1232
return this.negated;
1233
}
1234
}
1235
1236
export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression {
1237
1238
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1239
return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated));
1240
}
1241
1242
public readonly type = ContextKeyExprType.GreaterEquals;
1243
1244
private constructor(
1245
private readonly key: string,
1246
private readonly value: number | string,
1247
private negated: ContextKeyExpression | null
1248
) { }
1249
1250
public cmp(other: ContextKeyExpression): number {
1251
if (other.type !== this.type) {
1252
return this.type - other.type;
1253
}
1254
return cmp2(this.key, this.value, other.key, other.value);
1255
}
1256
1257
public equals(other: ContextKeyExpression): boolean {
1258
if (other.type === this.type) {
1259
return (this.key === other.key && this.value === other.value);
1260
}
1261
return false;
1262
}
1263
1264
public substituteConstants(): ContextKeyExpression | undefined {
1265
return this;
1266
}
1267
1268
public evaluate(context: IContext): boolean {
1269
if (typeof this.value === 'string') {
1270
return false;
1271
}
1272
return (parseFloat(context.getValue<any>(this.key)) >= this.value);
1273
}
1274
1275
public serialize(): string {
1276
return `${this.key} >= ${this.value}`;
1277
}
1278
1279
public keys(): string[] {
1280
return [this.key];
1281
}
1282
1283
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1284
return mapFnc.mapGreaterEquals(this.key, this.value);
1285
}
1286
1287
public negate(): ContextKeyExpression {
1288
if (!this.negated) {
1289
this.negated = ContextKeySmallerExpr.create(this.key, this.value, this);
1290
}
1291
return this.negated;
1292
}
1293
}
1294
1295
export class ContextKeySmallerExpr implements IContextKeyExpression {
1296
1297
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1298
return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated));
1299
}
1300
1301
public readonly type = ContextKeyExprType.Smaller;
1302
1303
private constructor(
1304
private readonly key: string,
1305
private readonly value: number | string,
1306
private negated: ContextKeyExpression | null
1307
) {
1308
}
1309
1310
public cmp(other: ContextKeyExpression): number {
1311
if (other.type !== this.type) {
1312
return this.type - other.type;
1313
}
1314
return cmp2(this.key, this.value, other.key, other.value);
1315
}
1316
1317
public equals(other: ContextKeyExpression): boolean {
1318
if (other.type === this.type) {
1319
return (this.key === other.key && this.value === other.value);
1320
}
1321
return false;
1322
}
1323
1324
public substituteConstants(): ContextKeyExpression | undefined {
1325
return this;
1326
}
1327
1328
public evaluate(context: IContext): boolean {
1329
if (typeof this.value === 'string') {
1330
return false;
1331
}
1332
return (parseFloat(context.getValue<any>(this.key)) < this.value);
1333
}
1334
1335
public serialize(): string {
1336
return `${this.key} < ${this.value}`;
1337
}
1338
1339
public keys(): string[] {
1340
return [this.key];
1341
}
1342
1343
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1344
return mapFnc.mapSmaller(this.key, this.value);
1345
}
1346
1347
public negate(): ContextKeyExpression {
1348
if (!this.negated) {
1349
this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this);
1350
}
1351
return this.negated;
1352
}
1353
}
1354
1355
export class ContextKeySmallerEqualsExpr implements IContextKeyExpression {
1356
1357
public static create(key: string, _value: any, negated: ContextKeyExpression | null = null): ContextKeyExpression {
1358
return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated));
1359
}
1360
1361
public readonly type = ContextKeyExprType.SmallerEquals;
1362
1363
private constructor(
1364
private readonly key: string,
1365
private readonly value: number | string,
1366
private negated: ContextKeyExpression | null
1367
) {
1368
}
1369
1370
public cmp(other: ContextKeyExpression): number {
1371
if (other.type !== this.type) {
1372
return this.type - other.type;
1373
}
1374
return cmp2(this.key, this.value, other.key, other.value);
1375
}
1376
1377
public equals(other: ContextKeyExpression): boolean {
1378
if (other.type === this.type) {
1379
return (this.key === other.key && this.value === other.value);
1380
}
1381
return false;
1382
}
1383
1384
public substituteConstants(): ContextKeyExpression | undefined {
1385
return this;
1386
}
1387
1388
public evaluate(context: IContext): boolean {
1389
if (typeof this.value === 'string') {
1390
return false;
1391
}
1392
return (parseFloat(context.getValue<any>(this.key)) <= this.value);
1393
}
1394
1395
public serialize(): string {
1396
return `${this.key} <= ${this.value}`;
1397
}
1398
1399
public keys(): string[] {
1400
return [this.key];
1401
}
1402
1403
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1404
return mapFnc.mapSmallerEquals(this.key, this.value);
1405
}
1406
1407
public negate(): ContextKeyExpression {
1408
if (!this.negated) {
1409
this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this);
1410
}
1411
return this.negated;
1412
}
1413
}
1414
1415
export class ContextKeyRegexExpr implements IContextKeyExpression {
1416
1417
public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr {
1418
return new ContextKeyRegexExpr(key, regexp);
1419
}
1420
1421
public readonly type = ContextKeyExprType.Regex;
1422
private negated: ContextKeyExpression | null = null;
1423
1424
private constructor(
1425
private readonly key: string,
1426
private readonly regexp: RegExp | null
1427
) {
1428
//
1429
}
1430
1431
public cmp(other: ContextKeyExpression): number {
1432
if (other.type !== this.type) {
1433
return this.type - other.type;
1434
}
1435
if (this.key < other.key) {
1436
return -1;
1437
}
1438
if (this.key > other.key) {
1439
return 1;
1440
}
1441
const thisSource = this.regexp ? this.regexp.source : '';
1442
const otherSource = other.regexp ? other.regexp.source : '';
1443
if (thisSource < otherSource) {
1444
return -1;
1445
}
1446
if (thisSource > otherSource) {
1447
return 1;
1448
}
1449
return 0;
1450
}
1451
1452
public equals(other: ContextKeyExpression): boolean {
1453
if (other.type === this.type) {
1454
const thisSource = this.regexp ? this.regexp.source : '';
1455
const otherSource = other.regexp ? other.regexp.source : '';
1456
return (this.key === other.key && thisSource === otherSource);
1457
}
1458
return false;
1459
}
1460
1461
public substituteConstants(): ContextKeyExpression | undefined {
1462
return this;
1463
}
1464
1465
public evaluate(context: IContext): boolean {
1466
const value = context.getValue<any>(this.key);
1467
return this.regexp ? this.regexp.test(value) : false;
1468
}
1469
1470
public serialize(): string {
1471
const value = this.regexp
1472
? `/${this.regexp.source}/${this.regexp.flags}`
1473
: '/invalid/';
1474
return `${this.key} =~ ${value}`;
1475
}
1476
1477
public keys(): string[] {
1478
return [this.key];
1479
}
1480
1481
public map(mapFnc: IContextKeyExprMapper): ContextKeyRegexExpr {
1482
return mapFnc.mapRegex(this.key, this.regexp);
1483
}
1484
1485
public negate(): ContextKeyExpression {
1486
if (!this.negated) {
1487
this.negated = ContextKeyNotRegexExpr.create(this);
1488
}
1489
return this.negated;
1490
}
1491
}
1492
1493
export class ContextKeyNotRegexExpr implements IContextKeyExpression {
1494
1495
public static create(actual: ContextKeyRegexExpr): ContextKeyExpression {
1496
return new ContextKeyNotRegexExpr(actual);
1497
}
1498
1499
public readonly type = ContextKeyExprType.NotRegex;
1500
1501
private constructor(private readonly _actual: ContextKeyRegexExpr) {
1502
//
1503
}
1504
1505
public cmp(other: ContextKeyExpression): number {
1506
if (other.type !== this.type) {
1507
return this.type - other.type;
1508
}
1509
return this._actual.cmp(other._actual);
1510
}
1511
1512
public equals(other: ContextKeyExpression): boolean {
1513
if (other.type === this.type) {
1514
return this._actual.equals(other._actual);
1515
}
1516
return false;
1517
}
1518
1519
public substituteConstants(): ContextKeyExpression | undefined {
1520
return this;
1521
}
1522
1523
public evaluate(context: IContext): boolean {
1524
return !this._actual.evaluate(context);
1525
}
1526
1527
public serialize(): string {
1528
return `!(${this._actual.serialize()})`;
1529
}
1530
1531
public keys(): string[] {
1532
return this._actual.keys();
1533
}
1534
1535
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1536
return new ContextKeyNotRegexExpr(this._actual.map(mapFnc));
1537
}
1538
1539
public negate(): ContextKeyExpression {
1540
return this._actual;
1541
}
1542
}
1543
1544
/**
1545
* @returns the same instance if nothing changed.
1546
*/
1547
function eliminateConstantsInArray(arr: ContextKeyExpression[]): (ContextKeyExpression | undefined)[] {
1548
// Allocate array only if there is a difference
1549
let newArr: (ContextKeyExpression | undefined)[] | null = null;
1550
for (let i = 0, len = arr.length; i < len; i++) {
1551
const newExpr = arr[i].substituteConstants();
1552
1553
if (arr[i] !== newExpr) {
1554
// something has changed!
1555
1556
// allocate array on first difference
1557
if (newArr === null) {
1558
newArr = [];
1559
for (let j = 0; j < i; j++) {
1560
newArr[j] = arr[j];
1561
}
1562
}
1563
}
1564
1565
if (newArr !== null) {
1566
newArr[i] = newExpr;
1567
}
1568
}
1569
1570
if (newArr === null) {
1571
return arr;
1572
}
1573
return newArr;
1574
}
1575
1576
export class ContextKeyAndExpr implements IContextKeyExpression {
1577
1578
public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1579
return ContextKeyAndExpr._normalizeArr(_expr, negated, extraRedundantCheck);
1580
}
1581
1582
public readonly type = ContextKeyExprType.And;
1583
1584
private constructor(
1585
public readonly expr: ContextKeyExpression[],
1586
private negated: ContextKeyExpression | null
1587
) {
1588
}
1589
1590
public cmp(other: ContextKeyExpression): number {
1591
if (other.type !== this.type) {
1592
return this.type - other.type;
1593
}
1594
if (this.expr.length < other.expr.length) {
1595
return -1;
1596
}
1597
if (this.expr.length > other.expr.length) {
1598
return 1;
1599
}
1600
for (let i = 0, len = this.expr.length; i < len; i++) {
1601
const r = cmp(this.expr[i], other.expr[i]);
1602
if (r !== 0) {
1603
return r;
1604
}
1605
}
1606
return 0;
1607
}
1608
1609
public equals(other: ContextKeyExpression): boolean {
1610
if (other.type === this.type) {
1611
if (this.expr.length !== other.expr.length) {
1612
return false;
1613
}
1614
for (let i = 0, len = this.expr.length; i < len; i++) {
1615
if (!this.expr[i].equals(other.expr[i])) {
1616
return false;
1617
}
1618
}
1619
return true;
1620
}
1621
return false;
1622
}
1623
1624
public substituteConstants(): ContextKeyExpression | undefined {
1625
const exprArr = eliminateConstantsInArray(this.expr);
1626
if (exprArr === this.expr) {
1627
// no change
1628
return this;
1629
}
1630
return ContextKeyAndExpr.create(exprArr, this.negated, false);
1631
}
1632
1633
public evaluate(context: IContext): boolean {
1634
for (let i = 0, len = this.expr.length; i < len; i++) {
1635
if (!this.expr[i].evaluate(context)) {
1636
return false;
1637
}
1638
}
1639
return true;
1640
}
1641
1642
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1643
const expr: ContextKeyExpression[] = [];
1644
let hasTrue = false;
1645
1646
for (const e of arr) {
1647
if (!e) {
1648
continue;
1649
}
1650
1651
if (e.type === ContextKeyExprType.True) {
1652
// anything && true ==> anything
1653
hasTrue = true;
1654
continue;
1655
}
1656
1657
if (e.type === ContextKeyExprType.False) {
1658
// anything && false ==> false
1659
return ContextKeyFalseExpr.INSTANCE;
1660
}
1661
1662
if (e.type === ContextKeyExprType.And) {
1663
expr.push(...e.expr);
1664
continue;
1665
}
1666
1667
expr.push(e);
1668
}
1669
1670
if (expr.length === 0 && hasTrue) {
1671
return ContextKeyTrueExpr.INSTANCE;
1672
}
1673
1674
if (expr.length === 0) {
1675
return undefined;
1676
}
1677
1678
if (expr.length === 1) {
1679
return expr[0];
1680
}
1681
1682
expr.sort(cmp);
1683
1684
// eliminate duplicate terms
1685
for (let i = 1; i < expr.length; i++) {
1686
if (expr[i - 1].equals(expr[i])) {
1687
expr.splice(i, 1);
1688
i--;
1689
}
1690
}
1691
1692
if (expr.length === 1) {
1693
return expr[0];
1694
}
1695
1696
// We must distribute any OR expression because we don't support parens
1697
// OR extensions will be at the end (due to sorting rules)
1698
while (expr.length > 1) {
1699
const lastElement = expr[expr.length - 1];
1700
if (lastElement.type !== ContextKeyExprType.Or) {
1701
break;
1702
}
1703
// pop the last element
1704
expr.pop();
1705
1706
// pop the second to last element
1707
const secondToLastElement = expr.pop()!;
1708
1709
const isFinished = (expr.length === 0);
1710
1711
// distribute `lastElement` over `secondToLastElement`
1712
const resultElement = ContextKeyOrExpr.create(
1713
lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement], null, extraRedundantCheck)),
1714
null,
1715
isFinished
1716
);
1717
1718
if (resultElement) {
1719
expr.push(resultElement);
1720
expr.sort(cmp);
1721
}
1722
}
1723
1724
if (expr.length === 1) {
1725
return expr[0];
1726
}
1727
1728
// resolve false AND expressions
1729
if (extraRedundantCheck) {
1730
for (let i = 0; i < expr.length; i++) {
1731
for (let j = i + 1; j < expr.length; j++) {
1732
if (expr[i].negate().equals(expr[j])) {
1733
// A && !A case
1734
return ContextKeyFalseExpr.INSTANCE;
1735
}
1736
}
1737
}
1738
1739
if (expr.length === 1) {
1740
return expr[0];
1741
}
1742
}
1743
1744
return new ContextKeyAndExpr(expr, negated);
1745
}
1746
1747
public serialize(): string {
1748
return this.expr.map(e => e.serialize()).join(' && ');
1749
}
1750
1751
public keys(): string[] {
1752
const result: string[] = [];
1753
for (const expr of this.expr) {
1754
result.push(...expr.keys());
1755
}
1756
return result;
1757
}
1758
1759
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1760
return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc)), null);
1761
}
1762
1763
public negate(): ContextKeyExpression {
1764
if (!this.negated) {
1765
const result: ContextKeyExpression[] = [];
1766
for (const expr of this.expr) {
1767
result.push(expr.negate());
1768
}
1769
this.negated = ContextKeyOrExpr.create(result, this, true)!;
1770
}
1771
return this.negated;
1772
}
1773
}
1774
1775
export class ContextKeyOrExpr implements IContextKeyExpression {
1776
1777
public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1778
return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck);
1779
}
1780
1781
public readonly type = ContextKeyExprType.Or;
1782
1783
private constructor(
1784
public readonly expr: ContextKeyExpression[],
1785
private negated: ContextKeyExpression | null
1786
) {
1787
}
1788
1789
public cmp(other: ContextKeyExpression): number {
1790
if (other.type !== this.type) {
1791
return this.type - other.type;
1792
}
1793
if (this.expr.length < other.expr.length) {
1794
return -1;
1795
}
1796
if (this.expr.length > other.expr.length) {
1797
return 1;
1798
}
1799
for (let i = 0, len = this.expr.length; i < len; i++) {
1800
const r = cmp(this.expr[i], other.expr[i]);
1801
if (r !== 0) {
1802
return r;
1803
}
1804
}
1805
return 0;
1806
}
1807
1808
public equals(other: ContextKeyExpression): boolean {
1809
if (other.type === this.type) {
1810
if (this.expr.length !== other.expr.length) {
1811
return false;
1812
}
1813
for (let i = 0, len = this.expr.length; i < len; i++) {
1814
if (!this.expr[i].equals(other.expr[i])) {
1815
return false;
1816
}
1817
}
1818
return true;
1819
}
1820
return false;
1821
}
1822
1823
public substituteConstants(): ContextKeyExpression | undefined {
1824
const exprArr = eliminateConstantsInArray(this.expr);
1825
if (exprArr === this.expr) {
1826
// no change
1827
return this;
1828
}
1829
return ContextKeyOrExpr.create(exprArr, this.negated, false);
1830
}
1831
1832
public evaluate(context: IContext): boolean {
1833
for (let i = 0, len = this.expr.length; i < len; i++) {
1834
if (this.expr[i].evaluate(context)) {
1835
return true;
1836
}
1837
}
1838
return false;
1839
}
1840
1841
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>, negated: ContextKeyExpression | null, extraRedundantCheck: boolean): ContextKeyExpression | undefined {
1842
let expr: ContextKeyExpression[] = [];
1843
let hasFalse = false;
1844
1845
if (arr) {
1846
for (let i = 0, len = arr.length; i < len; i++) {
1847
const e = arr[i];
1848
if (!e) {
1849
continue;
1850
}
1851
1852
if (e.type === ContextKeyExprType.False) {
1853
// anything || false ==> anything
1854
hasFalse = true;
1855
continue;
1856
}
1857
1858
if (e.type === ContextKeyExprType.True) {
1859
// anything || true ==> true
1860
return ContextKeyTrueExpr.INSTANCE;
1861
}
1862
1863
if (e.type === ContextKeyExprType.Or) {
1864
expr = expr.concat(e.expr);
1865
continue;
1866
}
1867
1868
expr.push(e);
1869
}
1870
1871
if (expr.length === 0 && hasFalse) {
1872
return ContextKeyFalseExpr.INSTANCE;
1873
}
1874
1875
expr.sort(cmp);
1876
}
1877
1878
if (expr.length === 0) {
1879
return undefined;
1880
}
1881
1882
if (expr.length === 1) {
1883
return expr[0];
1884
}
1885
1886
// eliminate duplicate terms
1887
for (let i = 1; i < expr.length; i++) {
1888
if (expr[i - 1].equals(expr[i])) {
1889
expr.splice(i, 1);
1890
i--;
1891
}
1892
}
1893
1894
if (expr.length === 1) {
1895
return expr[0];
1896
}
1897
1898
// resolve true OR expressions
1899
if (extraRedundantCheck) {
1900
for (let i = 0; i < expr.length; i++) {
1901
for (let j = i + 1; j < expr.length; j++) {
1902
if (expr[i].negate().equals(expr[j])) {
1903
// A || !A case
1904
return ContextKeyTrueExpr.INSTANCE;
1905
}
1906
}
1907
}
1908
1909
if (expr.length === 1) {
1910
return expr[0];
1911
}
1912
}
1913
1914
return new ContextKeyOrExpr(expr, negated);
1915
}
1916
1917
public serialize(): string {
1918
return this.expr.map(e => e.serialize()).join(' || ');
1919
}
1920
1921
public keys(): string[] {
1922
const result: string[] = [];
1923
for (const expr of this.expr) {
1924
result.push(...expr.keys());
1925
}
1926
return result;
1927
}
1928
1929
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
1930
return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc)), null);
1931
}
1932
1933
public negate(): ContextKeyExpression {
1934
if (!this.negated) {
1935
const result: ContextKeyExpression[] = [];
1936
for (const expr of this.expr) {
1937
result.push(expr.negate());
1938
}
1939
1940
// We don't support parens, so here we distribute the AND over the OR terminals
1941
// We always take the first 2 AND pairs and distribute them
1942
while (result.length > 1) {
1943
const LEFT = result.shift()!;
1944
const RIGHT = result.shift()!;
1945
1946
const all: ContextKeyExpression[] = [];
1947
for (const left of getTerminals(LEFT)) {
1948
for (const right of getTerminals(RIGHT)) {
1949
all.push(ContextKeyAndExpr.create([left, right], null, false)!);
1950
}
1951
}
1952
1953
result.unshift(ContextKeyOrExpr.create(all, null, false)!);
1954
}
1955
1956
this.negated = ContextKeyOrExpr.create(result, this, true)!;
1957
}
1958
return this.negated;
1959
}
1960
}
1961
1962
export interface ContextKeyInfo {
1963
readonly key: string;
1964
readonly type?: string;
1965
readonly description?: string;
1966
}
1967
1968
export class RawContextKey<T extends ContextKeyValue> extends ContextKeyDefinedExpr {
1969
1970
private static _info: ContextKeyInfo[] = [];
1971
1972
static all(): IterableIterator<ContextKeyInfo> {
1973
return RawContextKey._info.values();
1974
}
1975
1976
private readonly _defaultValue: T | undefined;
1977
1978
constructor(key: string, defaultValue: T | undefined, metaOrHide?: string | true | { type: string; description: string }) {
1979
super(key, null);
1980
this._defaultValue = defaultValue;
1981
1982
// collect all context keys into a central place
1983
if (typeof metaOrHide === 'object') {
1984
RawContextKey._info.push({ ...metaOrHide, key });
1985
} else if (metaOrHide !== true) {
1986
RawContextKey._info.push({ key, description: metaOrHide, type: defaultValue !== null && defaultValue !== undefined ? typeof defaultValue : undefined });
1987
}
1988
}
1989
1990
public bindTo(target: IContextKeyService): IContextKey<T> {
1991
return target.createKey(this.key, this._defaultValue);
1992
}
1993
1994
public getValue(target: IContextKeyService): T | undefined {
1995
return target.getContextKeyValue<T>(this.key);
1996
}
1997
1998
public toNegated(): ContextKeyExpression {
1999
return this.negate();
2000
}
2001
2002
public isEqualTo(value: any): ContextKeyExpression {
2003
return ContextKeyEqualsExpr.create(this.key, value);
2004
}
2005
2006
public notEqualsTo(value: any): ContextKeyExpression {
2007
return ContextKeyNotEqualsExpr.create(this.key, value);
2008
}
2009
2010
public greater(value: any): ContextKeyExpression {
2011
return ContextKeyGreaterExpr.create(this.key, value);
2012
}
2013
}
2014
2015
export type ContextKeyValue = null | undefined | boolean | number | string
2016
| Array<null | undefined | boolean | number | string>
2017
| Record<string, null | undefined | boolean | number | string>;
2018
2019
export interface IContext {
2020
getValue<T extends ContextKeyValue = ContextKeyValue>(key: string): T | undefined;
2021
}
2022
2023
export interface IContextKey<T extends ContextKeyValue = ContextKeyValue> {
2024
set(value: T): void;
2025
reset(): void;
2026
get(): T | undefined;
2027
}
2028
2029
export interface IContextKeyServiceTarget {
2030
parentElement: IContextKeyServiceTarget | null;
2031
setAttribute(attr: string, value: string): void;
2032
removeAttribute(attr: string): void;
2033
hasAttribute(attr: string): boolean;
2034
getAttribute(attr: string): string | null;
2035
}
2036
2037
export const IContextKeyService = createDecorator<IContextKeyService>('contextKeyService');
2038
2039
export interface IReadableSet<T> {
2040
has(value: T): boolean;
2041
}
2042
2043
export interface IContextKeyChangeEvent {
2044
affectsSome(keys: IReadableSet<string>): boolean;
2045
allKeysContainedIn(keys: IReadableSet<string>): boolean;
2046
}
2047
2048
export type IScopedContextKeyService = IContextKeyService & IDisposable;
2049
2050
export interface IContextKeyService {
2051
readonly _serviceBrand: undefined;
2052
2053
readonly onDidChangeContext: Event<IContextKeyChangeEvent>;
2054
bufferChangeEvents(callback: Function): void;
2055
2056
createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T>;
2057
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean;
2058
getContextKeyValue<T>(key: string): T | undefined;
2059
2060
createScoped(target: IContextKeyServiceTarget): IScopedContextKeyService;
2061
createOverlay(overlay: Iterable<[string, any]>): IContextKeyService;
2062
getContext(target: IContextKeyServiceTarget | null): IContext;
2063
2064
updateParent(parentContextKeyService: IContextKeyService): void;
2065
}
2066
2067
function cmp1(key1: string, key2: string): number {
2068
if (key1 < key2) {
2069
return -1;
2070
}
2071
if (key1 > key2) {
2072
return 1;
2073
}
2074
return 0;
2075
}
2076
2077
function cmp2(key1: string, value1: any, key2: string, value2: any): number {
2078
if (key1 < key2) {
2079
return -1;
2080
}
2081
if (key1 > key2) {
2082
return 1;
2083
}
2084
if (value1 < value2) {
2085
return -1;
2086
}
2087
if (value1 > value2) {
2088
return 1;
2089
}
2090
return 0;
2091
}
2092
2093
/**
2094
* Returns true if it is provable `p` implies `q`.
2095
*/
2096
export function implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean {
2097
2098
if (p.type === ContextKeyExprType.False || q.type === ContextKeyExprType.True) {
2099
// false implies anything
2100
// anything implies true
2101
return true;
2102
}
2103
2104
if (p.type === ContextKeyExprType.Or) {
2105
if (q.type === ContextKeyExprType.Or) {
2106
// `a || b || c` can only imply something like `a || b || c || d`
2107
return allElementsIncluded(p.expr, q.expr);
2108
}
2109
return false;
2110
}
2111
2112
if (q.type === ContextKeyExprType.Or) {
2113
for (const element of q.expr) {
2114
if (implies(p, element)) {
2115
return true;
2116
}
2117
}
2118
return false;
2119
}
2120
2121
if (p.type === ContextKeyExprType.And) {
2122
if (q.type === ContextKeyExprType.And) {
2123
// `a && b && c` implies `a && c`
2124
return allElementsIncluded(q.expr, p.expr);
2125
}
2126
for (const element of p.expr) {
2127
if (implies(element, q)) {
2128
return true;
2129
}
2130
}
2131
return false;
2132
}
2133
2134
return p.equals(q);
2135
}
2136
2137
/**
2138
* Returns true if all elements in `p` are also present in `q`.
2139
* The two arrays are assumed to be sorted
2140
*/
2141
function allElementsIncluded(p: ContextKeyExpression[], q: ContextKeyExpression[]): boolean {
2142
let pIndex = 0;
2143
let qIndex = 0;
2144
while (pIndex < p.length && qIndex < q.length) {
2145
const cmp = p[pIndex].cmp(q[qIndex]);
2146
2147
if (cmp < 0) {
2148
// an element from `p` is missing from `q`
2149
return false;
2150
} else if (cmp === 0) {
2151
pIndex++;
2152
qIndex++;
2153
} else {
2154
qIndex++;
2155
}
2156
}
2157
return (pIndex === p.length);
2158
}
2159
2160
function getTerminals(node: ContextKeyExpression) {
2161
if (node.type === ContextKeyExprType.Or) {
2162
return node.expr;
2163
}
2164
return [node];
2165
}
2166
2167