UNPKG

monaco-editor-core

Version:

A browser based code editor

1,410 lines • 54 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { isChrome, isEdge, isFirefox, isLinux, isMacintosh, isSafari, isWeb, isWindows } from '../../../base/common/platform.js'; import { isFalsyOrWhitespace } from '../../../base/common/strings.js'; import { Scanner } from './scanner.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { localize } from '../../../nls.js'; const CONSTANT_VALUES = new Map(); CONSTANT_VALUES.set('false', false); CONSTANT_VALUES.set('true', true); CONSTANT_VALUES.set('isMac', isMacintosh); CONSTANT_VALUES.set('isLinux', isLinux); CONSTANT_VALUES.set('isWindows', isWindows); CONSTANT_VALUES.set('isWeb', isWeb); CONSTANT_VALUES.set('isMacNative', isMacintosh && !isWeb); CONSTANT_VALUES.set('isEdge', isEdge); CONSTANT_VALUES.set('isFirefox', isFirefox); CONSTANT_VALUES.set('isChrome', isChrome); CONSTANT_VALUES.set('isSafari', isSafari); const hasOwnProperty = Object.prototype.hasOwnProperty; const defaultConfig = { regexParsingWithErrorRecovery: true }; const errorEmptyString = localize('contextkey.parser.error.emptyString', "Empty context key expression"); 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."); const errorNoInAfterNot = localize('contextkey.parser.error.noInAfterNot', "'in' after 'not'."); const errorClosingParenthesis = localize('contextkey.parser.error.closingParenthesis', "closing parenthesis ')'"); const errorUnexpectedToken = localize('contextkey.parser.error.unexpectedToken', "Unexpected token"); const hintUnexpectedToken = localize('contextkey.parser.error.unexpectedToken.hint', "Did you forget to put && or || before the token?"); const errorUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF', "Unexpected end of expression"); const hintUnexpectedEOF = localize('contextkey.parser.error.unexpectedEOF.hint', "Did you forget to put a context key?"); /** * A parser for context key expressions. * * Example: * ```ts * const parser = new Parser(); * const expr = parser.parse('foo == "bar" && baz == true'); * * if (expr === undefined) { * // there were lexing or parsing errors * // process lexing errors with `parser.lexingErrors` * // process parsing errors with `parser.parsingErrors` * } else { * // expr is a valid expression * } * ``` */ export class Parser { // Note: this doesn't produce an exact syntax tree but a normalized one // ContextKeyExpression's that we use as AST nodes do not expose constructors that do not normalize static { this._parseError = new Error(); } constructor(_config = defaultConfig) { this._config = _config; // lifetime note: `_scanner` lives as long as the parser does, i.e., is not reset between calls to `parse` this._scanner = new Scanner(); // lifetime note: `_tokens`, `_current`, and `_parsingErrors` must be reset between calls to `parse` this._tokens = []; this._current = 0; // invariant: 0 <= this._current < this._tokens.length ; any incrementation of this value must first call `_isAtEnd` this._parsingErrors = []; this._flagsGYRe = /g|y/g; } /** * Parse a context key expression. * * @param input the expression to parse * @returns the parsed expression or `undefined` if there's an error - call `lexingErrors` and `parsingErrors` to see the errors */ parse(input) { if (input === '') { this._parsingErrors.push({ message: errorEmptyString, offset: 0, lexeme: '', additionalInfo: hintEmptyString }); return undefined; } this._tokens = this._scanner.reset(input).scan(); // @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 this._current = 0; this._parsingErrors = []; try { const expr = this._expr(); if (!this._isAtEnd()) { const peek = this._peek(); const additionalInfo = peek.type === 17 /* TokenType.Str */ ? hintUnexpectedToken : undefined; this._parsingErrors.push({ message: errorUnexpectedToken, offset: peek.offset, lexeme: Scanner.getLexeme(peek), additionalInfo }); throw Parser._parseError; } return expr; } catch (e) { if (!(e === Parser._parseError)) { throw e; } return undefined; } } _expr() { return this._or(); } _or() { const expr = [this._and()]; while (this._matchOne(16 /* TokenType.Or */)) { const right = this._and(); expr.push(right); } return expr.length === 1 ? expr[0] : ContextKeyExpr.or(...expr); } _and() { const expr = [this._term()]; while (this._matchOne(15 /* TokenType.And */)) { const right = this._term(); expr.push(right); } return expr.length === 1 ? expr[0] : ContextKeyExpr.and(...expr); } _term() { if (this._matchOne(2 /* TokenType.Neg */)) { const peek = this._peek(); switch (peek.type) { case 11 /* TokenType.True */: this._advance(); return ContextKeyFalseExpr.INSTANCE; case 12 /* TokenType.False */: this._advance(); return ContextKeyTrueExpr.INSTANCE; case 0 /* TokenType.LParen */: { this._advance(); const expr = this._expr(); this._consume(1 /* TokenType.RParen */, errorClosingParenthesis); return expr?.negate(); } case 17 /* TokenType.Str */: this._advance(); return ContextKeyNotExpr.create(peek.lexeme); default: throw this._errExpectedButGot(`KEY | true | false | '(' expression ')'`, peek); } } return this._primary(); } _primary() { const peek = this._peek(); switch (peek.type) { case 11 /* TokenType.True */: this._advance(); return ContextKeyExpr.true(); case 12 /* TokenType.False */: this._advance(); return ContextKeyExpr.false(); case 0 /* TokenType.LParen */: { this._advance(); const expr = this._expr(); this._consume(1 /* TokenType.RParen */, errorClosingParenthesis); return expr; } case 17 /* TokenType.Str */: { // KEY const key = peek.lexeme; this._advance(); // =~ regex if (this._matchOne(9 /* TokenType.RegexOp */)) { // @ulugbekna: we need to reconstruct the regex from the tokens because some extensions use unescaped slashes in regexes const expr = this._peek(); if (!this._config.regexParsingWithErrorRecovery) { this._advance(); if (expr.type !== 10 /* TokenType.RegexStr */) { throw this._errExpectedButGot(`REGEX`, expr); } const regexLexeme = expr.lexeme; const closingSlashIndex = regexLexeme.lastIndexOf('/'); const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1)); let regexp; try { regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags); } catch (e) { throw this._errExpectedButGot(`REGEX`, expr); } return ContextKeyRegexExpr.create(key, regexp); } switch (expr.type) { case 10 /* TokenType.RegexStr */: case 19 /* TokenType.Error */: { // also handle an ErrorToken in case of smth such as /(/file)/ const lexemeReconstruction = [expr.lexeme]; // /REGEX/ or /REGEX/FLAGS this._advance(); let followingToken = this._peek(); let parenBalance = 0; for (let i = 0; i < expr.lexeme.length; i++) { if (expr.lexeme.charCodeAt(i) === 40 /* CharCode.OpenParen */) { parenBalance++; } else if (expr.lexeme.charCodeAt(i) === 41 /* CharCode.CloseParen */) { parenBalance--; } } while (!this._isAtEnd() && followingToken.type !== 15 /* TokenType.And */ && followingToken.type !== 16 /* TokenType.Or */) { switch (followingToken.type) { case 0 /* TokenType.LParen */: parenBalance++; break; case 1 /* TokenType.RParen */: parenBalance--; break; case 10 /* TokenType.RegexStr */: case 18 /* TokenType.QuotedStr */: for (let i = 0; i < followingToken.lexeme.length; i++) { if (followingToken.lexeme.charCodeAt(i) === 40 /* CharCode.OpenParen */) { parenBalance++; } else if (expr.lexeme.charCodeAt(i) === 41 /* CharCode.CloseParen */) { parenBalance--; } } } if (parenBalance < 0) { break; } lexemeReconstruction.push(Scanner.getLexeme(followingToken)); this._advance(); followingToken = this._peek(); } const regexLexeme = lexemeReconstruction.join(''); const closingSlashIndex = regexLexeme.lastIndexOf('/'); const flags = closingSlashIndex === regexLexeme.length - 1 ? undefined : this._removeFlagsGY(regexLexeme.substring(closingSlashIndex + 1)); let regexp; try { regexp = new RegExp(regexLexeme.substring(1, closingSlashIndex), flags); } catch (e) { throw this._errExpectedButGot(`REGEX`, expr); } return ContextKeyExpr.regex(key, regexp); } case 18 /* TokenType.QuotedStr */: { const serializedValue = expr.lexeme; this._advance(); // replicate old regex parsing behavior let regex = null; if (!isFalsyOrWhitespace(serializedValue)) { const start = serializedValue.indexOf('/'); const end = serializedValue.lastIndexOf('/'); if (start !== end && start >= 0) { const value = serializedValue.slice(start + 1, end); const caseIgnoreFlag = serializedValue[end + 1] === 'i' ? 'i' : ''; try { regex = new RegExp(value, caseIgnoreFlag); } catch (_e) { throw this._errExpectedButGot(`REGEX`, expr); } } } if (regex === null) { throw this._errExpectedButGot('REGEX', expr); } return ContextKeyRegexExpr.create(key, regex); } default: throw this._errExpectedButGot('REGEX', this._peek()); } } // [ 'not' 'in' value ] if (this._matchOne(14 /* TokenType.Not */)) { this._consume(13 /* TokenType.In */, errorNoInAfterNot); const right = this._value(); return ContextKeyExpr.notIn(key, right); } // [ ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in') value ] const maybeOp = this._peek().type; switch (maybeOp) { case 3 /* TokenType.Eq */: { this._advance(); const right = this._value(); if (this._previous().type === 18 /* TokenType.QuotedStr */) { // to preserve old parser behavior: "foo == 'true'" is preserved as "foo == 'true'", but "foo == true" is optimized as "foo" return ContextKeyExpr.equals(key, right); } switch (right) { case 'true': return ContextKeyExpr.has(key); case 'false': return ContextKeyExpr.not(key); default: return ContextKeyExpr.equals(key, right); } } case 4 /* TokenType.NotEq */: { this._advance(); const right = this._value(); if (this._previous().type === 18 /* TokenType.QuotedStr */) { // same as above with "foo != 'true'" return ContextKeyExpr.notEquals(key, right); } switch (right) { case 'true': return ContextKeyExpr.not(key); case 'false': return ContextKeyExpr.has(key); default: return ContextKeyExpr.notEquals(key, right); } } // 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 // consequently, package.json linter should _warn_ the user if they're passing undesired things to ops case 5 /* TokenType.Lt */: this._advance(); return ContextKeySmallerExpr.create(key, this._value()); case 6 /* TokenType.LtEq */: this._advance(); return ContextKeySmallerEqualsExpr.create(key, this._value()); case 7 /* TokenType.Gt */: this._advance(); return ContextKeyGreaterExpr.create(key, this._value()); case 8 /* TokenType.GtEq */: this._advance(); return ContextKeyGreaterEqualsExpr.create(key, this._value()); case 13 /* TokenType.In */: this._advance(); return ContextKeyExpr.in(key, this._value()); default: return ContextKeyExpr.has(key); } } case 20 /* TokenType.EOF */: this._parsingErrors.push({ message: errorUnexpectedEOF, offset: peek.offset, lexeme: '', additionalInfo: hintUnexpectedEOF }); throw Parser._parseError; default: throw this._errExpectedButGot(`true | false | KEY \n\t| KEY '=~' REGEX \n\t| KEY ('==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not' 'in') value`, this._peek()); } } _value() { const token = this._peek(); switch (token.type) { case 17 /* TokenType.Str */: case 18 /* TokenType.QuotedStr */: this._advance(); return token.lexeme; case 11 /* TokenType.True */: this._advance(); return 'true'; case 12 /* TokenType.False */: this._advance(); return 'false'; case 13 /* TokenType.In */: // we support `in` as a value, e.g., "when": "languageId == in" - exists in existing extensions this._advance(); return 'in'; default: // this allows "when": "foo == " which's used by existing extensions // we do not call `_advance` on purpose - we don't want to eat unintended tokens return ''; } } _removeFlagsGY(flags) { return flags.replaceAll(this._flagsGYRe, ''); } // careful: this can throw if current token is the initial one (ie index = 0) _previous() { return this._tokens[this._current - 1]; } _matchOne(token) { if (this._check(token)) { this._advance(); return true; } return false; } _advance() { if (!this._isAtEnd()) { this._current++; } return this._previous(); } _consume(type, message) { if (this._check(type)) { return this._advance(); } throw this._errExpectedButGot(message, this._peek()); } _errExpectedButGot(expected, got, additionalInfo) { const message = localize('contextkey.parser.error.expectedButGot', "Expected: {0}\nReceived: '{1}'.", expected, Scanner.getLexeme(got)); const offset = got.offset; const lexeme = Scanner.getLexeme(got); this._parsingErrors.push({ message, offset, lexeme, additionalInfo }); return Parser._parseError; } _check(type) { return this._peek().type === type; } _peek() { return this._tokens[this._current]; } _isAtEnd() { return this._peek().type === 20 /* TokenType.EOF */; } } export class ContextKeyExpr { static false() { return ContextKeyFalseExpr.INSTANCE; } static true() { return ContextKeyTrueExpr.INSTANCE; } static has(key) { return ContextKeyDefinedExpr.create(key); } static equals(key, value) { return ContextKeyEqualsExpr.create(key, value); } static notEquals(key, value) { return ContextKeyNotEqualsExpr.create(key, value); } static regex(key, value) { return ContextKeyRegexExpr.create(key, value); } static in(key, value) { return ContextKeyInExpr.create(key, value); } static notIn(key, value) { return ContextKeyNotInExpr.create(key, value); } static not(key) { return ContextKeyNotExpr.create(key); } static and(...expr) { return ContextKeyAndExpr.create(expr, null, true); } static or(...expr) { return ContextKeyOrExpr.create(expr, null, true); } static { this._parser = new Parser({ regexParsingWithErrorRecovery: false }); } static deserialize(serialized) { if (serialized === undefined || serialized === null) { // an empty string needs to be handled by the parser to get a corresponding parsing error reported return undefined; } const expr = this._parser.parse(serialized); return expr; } } export function expressionsAreEqualWithConstantSubstitution(a, b) { const aExpr = a ? a.substituteConstants() : undefined; const bExpr = b ? b.substituteConstants() : undefined; if (!aExpr && !bExpr) { return true; } if (!aExpr || !bExpr) { return false; } return aExpr.equals(bExpr); } function cmp(a, b) { return a.cmp(b); } export class ContextKeyFalseExpr { static { this.INSTANCE = new ContextKeyFalseExpr(); } constructor() { this.type = 0 /* ContextKeyExprType.False */; } cmp(other) { return this.type - other.type; } equals(other) { return (other.type === this.type); } substituteConstants() { return this; } evaluate(context) { return false; } serialize() { return 'false'; } keys() { return []; } negate() { return ContextKeyTrueExpr.INSTANCE; } } export class ContextKeyTrueExpr { static { this.INSTANCE = new ContextKeyTrueExpr(); } constructor() { this.type = 1 /* ContextKeyExprType.True */; } cmp(other) { return this.type - other.type; } equals(other) { return (other.type === this.type); } substituteConstants() { return this; } evaluate(context) { return true; } serialize() { return 'true'; } keys() { return []; } negate() { return ContextKeyFalseExpr.INSTANCE; } } export class ContextKeyDefinedExpr { static create(key, negated = null) { const constantValue = CONSTANT_VALUES.get(key); if (typeof constantValue === 'boolean') { return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; } return new ContextKeyDefinedExpr(key, negated); } constructor(key, negated) { this.key = key; this.negated = negated; this.type = 2 /* ContextKeyExprType.Defined */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp1(this.key, other.key); } equals(other) { if (other.type === this.type) { return (this.key === other.key); } return false; } substituteConstants() { const constantValue = CONSTANT_VALUES.get(this.key); if (typeof constantValue === 'boolean') { return constantValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; } return this; } evaluate(context) { return (!!context.getValue(this.key)); } serialize() { return this.key; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyNotExpr.create(this.key, this); } return this.negated; } } export class ContextKeyEqualsExpr { static create(key, value, negated = null) { if (typeof value === 'boolean') { return (value ? ContextKeyDefinedExpr.create(key, negated) : ContextKeyNotExpr.create(key, negated)); } const constantValue = CONSTANT_VALUES.get(key); if (typeof constantValue === 'boolean') { const trueValue = constantValue ? 'true' : 'false'; return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); } return new ContextKeyEqualsExpr(key, value, negated); } constructor(key, value, negated) { this.key = key; this.value = value; this.negated = negated; this.type = 4 /* ContextKeyExprType.Equals */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.value, other.key, other.value); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; } substituteConstants() { const constantValue = CONSTANT_VALUES.get(this.key); if (typeof constantValue === 'boolean') { const trueValue = constantValue ? 'true' : 'false'; return (this.value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); } return this; } evaluate(context) { // Intentional == // eslint-disable-next-line eqeqeq return (context.getValue(this.key) == this.value); } serialize() { return `${this.key} == '${this.value}'`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyNotEqualsExpr.create(this.key, this.value, this); } return this.negated; } } export class ContextKeyInExpr { static create(key, valueKey) { return new ContextKeyInExpr(key, valueKey); } constructor(key, valueKey) { this.key = key; this.valueKey = valueKey; this.type = 10 /* ContextKeyExprType.In */; this.negated = null; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.valueKey, other.key, other.valueKey); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.valueKey === other.valueKey); } return false; } substituteConstants() { return this; } evaluate(context) { const source = context.getValue(this.valueKey); const item = context.getValue(this.key); if (Array.isArray(source)) { return source.includes(item); } if (typeof item === 'string' && typeof source === 'object' && source !== null) { return hasOwnProperty.call(source, item); } return false; } serialize() { return `${this.key} in '${this.valueKey}'`; } keys() { return [this.key, this.valueKey]; } negate() { if (!this.negated) { this.negated = ContextKeyNotInExpr.create(this.key, this.valueKey); } return this.negated; } } export class ContextKeyNotInExpr { static create(key, valueKey) { return new ContextKeyNotInExpr(key, valueKey); } constructor(key, valueKey) { this.key = key; this.valueKey = valueKey; this.type = 11 /* ContextKeyExprType.NotIn */; this._negated = ContextKeyInExpr.create(key, valueKey); } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return this._negated.cmp(other._negated); } equals(other) { if (other.type === this.type) { return this._negated.equals(other._negated); } return false; } substituteConstants() { return this; } evaluate(context) { return !this._negated.evaluate(context); } serialize() { return `${this.key} not in '${this.valueKey}'`; } keys() { return this._negated.keys(); } negate() { return this._negated; } } export class ContextKeyNotEqualsExpr { static create(key, value, negated = null) { if (typeof value === 'boolean') { if (value) { return ContextKeyNotExpr.create(key, negated); } return ContextKeyDefinedExpr.create(key, negated); } const constantValue = CONSTANT_VALUES.get(key); if (typeof constantValue === 'boolean') { const falseValue = constantValue ? 'true' : 'false'; return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); } return new ContextKeyNotEqualsExpr(key, value, negated); } constructor(key, value, negated) { this.key = key; this.value = value; this.negated = negated; this.type = 5 /* ContextKeyExprType.NotEquals */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.value, other.key, other.value); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; } substituteConstants() { const constantValue = CONSTANT_VALUES.get(this.key); if (typeof constantValue === 'boolean') { const falseValue = constantValue ? 'true' : 'false'; return (this.value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); } return this; } evaluate(context) { // Intentional != // eslint-disable-next-line eqeqeq return (context.getValue(this.key) != this.value); } serialize() { return `${this.key} != '${this.value}'`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyEqualsExpr.create(this.key, this.value, this); } return this.negated; } } export class ContextKeyNotExpr { static create(key, negated = null) { const constantValue = CONSTANT_VALUES.get(key); if (typeof constantValue === 'boolean') { return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); } return new ContextKeyNotExpr(key, negated); } constructor(key, negated) { this.key = key; this.negated = negated; this.type = 3 /* ContextKeyExprType.Not */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp1(this.key, other.key); } equals(other) { if (other.type === this.type) { return (this.key === other.key); } return false; } substituteConstants() { const constantValue = CONSTANT_VALUES.get(this.key); if (typeof constantValue === 'boolean') { return (constantValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); } return this; } evaluate(context) { return (!context.getValue(this.key)); } serialize() { return `!${this.key}`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyDefinedExpr.create(this.key, this); } return this.negated; } } function withFloatOrStr(value, callback) { if (typeof value === 'string') { const n = parseFloat(value); if (!isNaN(n)) { value = n; } } if (typeof value === 'string' || typeof value === 'number') { return callback(value); } return ContextKeyFalseExpr.INSTANCE; } export class ContextKeyGreaterExpr { static create(key, _value, negated = null) { return withFloatOrStr(_value, (value) => new ContextKeyGreaterExpr(key, value, negated)); } constructor(key, value, negated) { this.key = key; this.value = value; this.negated = negated; this.type = 12 /* ContextKeyExprType.Greater */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.value, other.key, other.value); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; } substituteConstants() { return this; } evaluate(context) { if (typeof this.value === 'string') { return false; } return (parseFloat(context.getValue(this.key)) > this.value); } serialize() { return `${this.key} > ${this.value}`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeySmallerEqualsExpr.create(this.key, this.value, this); } return this.negated; } } export class ContextKeyGreaterEqualsExpr { static create(key, _value, negated = null) { return withFloatOrStr(_value, (value) => new ContextKeyGreaterEqualsExpr(key, value, negated)); } constructor(key, value, negated) { this.key = key; this.value = value; this.negated = negated; this.type = 13 /* ContextKeyExprType.GreaterEquals */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.value, other.key, other.value); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; } substituteConstants() { return this; } evaluate(context) { if (typeof this.value === 'string') { return false; } return (parseFloat(context.getValue(this.key)) >= this.value); } serialize() { return `${this.key} >= ${this.value}`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeySmallerExpr.create(this.key, this.value, this); } return this.negated; } } export class ContextKeySmallerExpr { static create(key, _value, negated = null) { return withFloatOrStr(_value, (value) => new ContextKeySmallerExpr(key, value, negated)); } constructor(key, value, negated) { this.key = key; this.value = value; this.negated = negated; this.type = 14 /* ContextKeyExprType.Smaller */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.value, other.key, other.value); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; } substituteConstants() { return this; } evaluate(context) { if (typeof this.value === 'string') { return false; } return (parseFloat(context.getValue(this.key)) < this.value); } serialize() { return `${this.key} < ${this.value}`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyGreaterEqualsExpr.create(this.key, this.value, this); } return this.negated; } } export class ContextKeySmallerEqualsExpr { static create(key, _value, negated = null) { return withFloatOrStr(_value, (value) => new ContextKeySmallerEqualsExpr(key, value, negated)); } constructor(key, value, negated) { this.key = key; this.value = value; this.negated = negated; this.type = 15 /* ContextKeyExprType.SmallerEquals */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return cmp2(this.key, this.value, other.key, other.value); } equals(other) { if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; } substituteConstants() { return this; } evaluate(context) { if (typeof this.value === 'string') { return false; } return (parseFloat(context.getValue(this.key)) <= this.value); } serialize() { return `${this.key} <= ${this.value}`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyGreaterExpr.create(this.key, this.value, this); } return this.negated; } } export class ContextKeyRegexExpr { static create(key, regexp) { return new ContextKeyRegexExpr(key, regexp); } constructor(key, regexp) { this.key = key; this.regexp = regexp; this.type = 7 /* ContextKeyExprType.Regex */; this.negated = null; // } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } if (this.key < other.key) { return -1; } if (this.key > other.key) { return 1; } const thisSource = this.regexp ? this.regexp.source : ''; const otherSource = other.regexp ? other.regexp.source : ''; if (thisSource < otherSource) { return -1; } if (thisSource > otherSource) { return 1; } return 0; } equals(other) { if (other.type === this.type) { const thisSource = this.regexp ? this.regexp.source : ''; const otherSource = other.regexp ? other.regexp.source : ''; return (this.key === other.key && thisSource === otherSource); } return false; } substituteConstants() { return this; } evaluate(context) { const value = context.getValue(this.key); return this.regexp ? this.regexp.test(value) : false; } serialize() { const value = this.regexp ? `/${this.regexp.source}/${this.regexp.flags}` : '/invalid/'; return `${this.key} =~ ${value}`; } keys() { return [this.key]; } negate() { if (!this.negated) { this.negated = ContextKeyNotRegexExpr.create(this); } return this.negated; } } export class ContextKeyNotRegexExpr { static create(actual) { return new ContextKeyNotRegexExpr(actual); } constructor(_actual) { this._actual = _actual; this.type = 8 /* ContextKeyExprType.NotRegex */; // } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } return this._actual.cmp(other._actual); } equals(other) { if (other.type === this.type) { return this._actual.equals(other._actual); } return false; } substituteConstants() { return this; } evaluate(context) { return !this._actual.evaluate(context); } serialize() { return `!(${this._actual.serialize()})`; } keys() { return this._actual.keys(); } negate() { return this._actual; } } /** * @returns the same instance if nothing changed. */ function eliminateConstantsInArray(arr) { // Allocate array only if there is a difference let newArr = null; for (let i = 0, len = arr.length; i < len; i++) { const newExpr = arr[i].substituteConstants(); if (arr[i] !== newExpr) { // something has changed! // allocate array on first difference if (newArr === null) { newArr = []; for (let j = 0; j < i; j++) { newArr[j] = arr[j]; } } } if (newArr !== null) { newArr[i] = newExpr; } } if (newArr === null) { return arr; } return newArr; } export class ContextKeyAndExpr { static create(_expr, negated, extraRedundantCheck) { return ContextKeyAndExpr._normalizeArr(_expr, negated, extraRedundantCheck); } constructor(expr, negated) { this.expr = expr; this.negated = negated; this.type = 6 /* ContextKeyExprType.And */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } if (this.expr.length < other.expr.length) { return -1; } if (this.expr.length > other.expr.length) { return 1; } for (let i = 0, len = this.expr.length; i < len; i++) { const r = cmp(this.expr[i], other.expr[i]); if (r !== 0) { return r; } } return 0; } equals(other) { if (other.type === this.type) { if (this.expr.length !== other.expr.length) { return false; } for (let i = 0, len = this.expr.length; i < len; i++) { if (!this.expr[i].equals(other.expr[i])) { return false; } } return true; } return false; } substituteConstants() { const exprArr = eliminateConstantsInArray(this.expr); if (exprArr === this.expr) { // no change return this; } return ContextKeyAndExpr.create(exprArr, this.negated, false); } evaluate(context) { for (let i = 0, len = this.expr.length; i < len; i++) { if (!this.expr[i].evaluate(context)) { return false; } } return true; } static _normalizeArr(arr, negated, extraRedundantCheck) { const expr = []; let hasTrue = false; for (const e of arr) { if (!e) { continue; } if (e.type === 1 /* ContextKeyExprType.True */) { // anything && true ==> anything hasTrue = true; continue; } if (e.type === 0 /* ContextKeyExprType.False */) { // anything && false ==> false return ContextKeyFalseExpr.INSTANCE; } if (e.type === 6 /* ContextKeyExprType.And */) { expr.push(...e.expr); continue; } expr.push(e); } if (expr.length === 0 && hasTrue) { return ContextKeyTrueExpr.INSTANCE; } if (expr.length === 0) { return undefined; } if (expr.length === 1) { return expr[0]; } expr.sort(cmp); // eliminate duplicate terms for (let i = 1; i < expr.length; i++) { if (expr[i - 1].equals(expr[i])) { expr.splice(i, 1); i--; } } if (expr.length === 1) { return expr[0]; } // We must distribute any OR expression because we don't support parens // OR extensions will be at the end (due to sorting rules) while (expr.length > 1) { const lastElement = expr[expr.length - 1]; if (lastElement.type !== 9 /* ContextKeyExprType.Or */) { break; } // pop the last element expr.pop(); // pop the second to last element const secondToLastElement = expr.pop(); const isFinished = (expr.length === 0); // distribute `lastElement` over `secondToLastElement` const resultElement = ContextKeyOrExpr.create(lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement], null, extraRedundantCheck)), null, isFinished); if (resultElement) { expr.push(resultElement); expr.sort(cmp); } } if (expr.length === 1) { return expr[0]; } // resolve false AND expressions if (extraRedundantCheck) { for (let i = 0; i < expr.length; i++) { for (let j = i + 1; j < expr.length; j++) { if (expr[i].negate().equals(expr[j])) { // A && !A case return ContextKeyFalseExpr.INSTANCE; } } } if (expr.length === 1) { return expr[0]; } } return new ContextKeyAndExpr(expr, negated); } serialize() { return this.expr.map(e => e.serialize()).join(' && '); } keys() { const result = []; for (const expr of this.expr) { result.push(...expr.keys()); } return result; } negate() { if (!this.negated) { const result = []; for (const expr of this.expr) { result.push(expr.negate()); } this.negated = ContextKeyOrExpr.create(result, this, true); } return this.negated; } } export class ContextKeyOrExpr { static create(_expr, negated, extraRedundantCheck) { return ContextKeyOrExpr._normalizeArr(_expr, negated, extraRedundantCheck); } constructor(expr, negated) { this.expr = expr; this.negated = negated; this.type = 9 /* ContextKeyExprType.Or */; } cmp(other) { if (other.type !== this.type) { return this.type - other.type; } if (this.expr.length < other.expr.length) { return -1; } if (this.expr.length > other.expr.length) { return 1; } for (let i = 0, len = this.expr.length; i < len; i++) { const r = cmp(this.expr[i], other.expr[i]); if (r !== 0) { return r; } } return 0; } equals(other) { if (other.type === this.type) { if (this.expr.length !== other.expr.length) { return false; } for (let i = 0, len = this.expr.length; i < len; i++) { if (!this.expr[i].equals(other.expr[i])) { return false; } } return true; } return false; } substituteConstants() { const exprArr = eliminateConstantsInArray(this.expr); if (exprArr === this.expr) { // no change return this; } return ContextKeyOrExpr.create(exprArr, this.negated, false); } evaluate(context) { for (let i = 0, len = this.expr.length; i < len; i++) { if (this.expr[i].evaluate(context)) { return true; } } return false; } static _normalizeArr(arr, negated, extraRedundantCheck) { let expr = []; let hasFalse = false; if (arr) { for (let i = 0, len = arr.length; i < len; i++) { const e = arr[i]; if (!e) { continue; } if (e.type === 0 /* ContextKeyExprType.False */) { // anything || false ==> anything hasFalse = true; continue; } if (e.type === 1 /* ContextKeyExprType.True */) { // anything || true ==> true return ContextKeyTrueExpr.INSTANCE; } if (e.type === 9 /* ContextKeyExprType.Or */) { expr = expr.concat(e.expr); continue; } expr.push(e); } if (expr.length === 0 && hasFalse) { return ContextKeyFalseExpr.INSTANCE; } expr.sort(cmp); } if (expr.length === 0) { return undefined; } if (expr.length === 1) { return expr[0]; } // eliminate duplicate terms for (let i = 1; i < expr.length; i++) { if (expr[i - 1].equals(expr[i])) { expr.splice(i, 1); i--; } } if (expr.length === 1) { return expr[0]; } // resolve true OR expressions if (extraRedundantCheck) { for (let i = 0; i < expr.length; i++) { for (let j = i + 1; j < expr.length; j++) { if (expr[i].negate().equals(expr[j])) { // A || !A case return ContextKeyTrueExpr.INSTANCE; } } } if (expr.length === 1) { return expr[0]; } } return new ContextKeyOrExpr(expr, negated); } serialize() { return this.expr.map(e => e.serialize()).join(' || '); } keys() { const result = []; for (const expr of this.expr) { result.push(...expr.keys()); } return result; } negate() { if (!this.negated) { const result = []; for (const expr of this.expr) { result.push(expr.negate()); } // We don't support parens, so here we distribute the AND over the OR terminals // We always take the first 2 AND pairs and distribu