Path: blob/main/src/vs/platform/contextkey/test/common/contextkey.test.ts
5256 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';6import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';7import { ContextKeyExpr, ContextKeyExpression, implies } from '../../common/contextkey.js';89function createContext(ctx: any) {10return {11getValue: (key: string) => {12return ctx[key];13}14};15}1617suite('ContextKeyExpr', () => {1819ensureNoDisposablesAreLeakedInTestSuite();2021test('ContextKeyExpr.equals', () => {22const a = ContextKeyExpr.and(23ContextKeyExpr.has('a1'),24ContextKeyExpr.and(ContextKeyExpr.has('and.a')),25ContextKeyExpr.has('a2'),26ContextKeyExpr.regex('d3', /d.*/),27ContextKeyExpr.regex('d4', /\*\*3*/),28ContextKeyExpr.equals('b1', 'bb1'),29ContextKeyExpr.equals('b2', 'bb2'),30ContextKeyExpr.notEquals('c1', 'cc1'),31ContextKeyExpr.notEquals('c2', 'cc2'),32ContextKeyExpr.not('d1'),33ContextKeyExpr.not('d2')34)!;35const b = ContextKeyExpr.and(36ContextKeyExpr.equals('b2', 'bb2'),37ContextKeyExpr.notEquals('c1', 'cc1'),38ContextKeyExpr.not('d1'),39ContextKeyExpr.regex('d4', /\*\*3*/),40ContextKeyExpr.notEquals('c2', 'cc2'),41ContextKeyExpr.has('a2'),42ContextKeyExpr.equals('b1', 'bb1'),43ContextKeyExpr.regex('d3', /d.*/),44ContextKeyExpr.has('a1'),45ContextKeyExpr.and(ContextKeyExpr.equals('and.a', true)),46ContextKeyExpr.not('d2')47)!;48assert(a.equals(b), 'expressions should be equal');49});5051test('issue #134942: Equals in comparator expressions', () => {52function testEquals(expr: ContextKeyExpression | undefined, str: string): void {53const deserialized = ContextKeyExpr.deserialize(str);54assert.ok(expr);55assert.ok(deserialized);56assert.strictEqual(expr.equals(deserialized), true, str);57}58testEquals(ContextKeyExpr.greater('value', 0), 'value > 0');59testEquals(ContextKeyExpr.greaterEquals('value', 0), 'value >= 0');60testEquals(ContextKeyExpr.smaller('value', 0), 'value < 0');61testEquals(ContextKeyExpr.smallerEquals('value', 0), 'value <= 0');62});6364test('normalize', () => {65const key1IsTrue = ContextKeyExpr.equals('key1', true);66const key1IsNotFalse = ContextKeyExpr.notEquals('key1', false);67const key1IsFalse = ContextKeyExpr.equals('key1', false);68const key1IsNotTrue = ContextKeyExpr.notEquals('key1', true);6970assert.ok(key1IsTrue.equals(ContextKeyExpr.has('key1')));71assert.ok(key1IsNotFalse.equals(ContextKeyExpr.has('key1')));72assert.ok(key1IsFalse.equals(ContextKeyExpr.not('key1')));73assert.ok(key1IsNotTrue.equals(ContextKeyExpr.not('key1')));74});7576test('evaluate', () => {77const context = createContext({78'a': true,79'b': false,80'c': '5',81'd': 'd'82});83function testExpression(expr: string, expected: boolean): void {84// console.log(expr + ' ' + expected);85const rules = ContextKeyExpr.deserialize(expr);86assert.strictEqual(rules!.evaluate(context), expected, expr);87}88function testBatch(expr: string, value: any): void {89/* eslint-disable eqeqeq */90testExpression(expr, !!value);91testExpression(expr + ' == true', !!value);92testExpression(expr + ' != true', !value);93testExpression(expr + ' == false', !value);94testExpression(expr + ' != false', !!value);95// eslint-disable-next-line local/code-no-any-casts96testExpression(expr + ' == 5', value == <any>'5');97// eslint-disable-next-line local/code-no-any-casts98testExpression(expr + ' != 5', value != <any>'5');99testExpression('!' + expr, !value);100testExpression(expr + ' =~ /d.*/', /d.*/.test(value));101testExpression(expr + ' =~ /D/i', /D/i.test(value));102/* eslint-enable eqeqeq */103}104105testBatch('a', true);106testBatch('b', false);107testBatch('c', '5');108testBatch('d', 'd');109testBatch('z', undefined);110111testExpression('true', true);112testExpression('false', false);113testExpression('a && !b', true && !false);114testExpression('a && b', true && false);115testExpression('a && !b && c == 5', true && !false && '5' === '5');116testExpression('d =~ /e.*/', false);117118// precedence test: false && true || true === true because && is evaluated first119testExpression('b && a || a', true);120121testExpression('a || b', true);122testExpression('b || b', false);123testExpression('b && a || a && b', false);124});125126test('negate', () => {127function testNegate(expr: string, expected: string): void {128const actual = ContextKeyExpr.deserialize(expr)!.negate().serialize();129assert.strictEqual(actual, expected);130}131testNegate('true', 'false');132testNegate('false', 'true');133testNegate('a', '!a');134testNegate('a && b || c', '!a && !c || !b && !c');135testNegate('a && b || c || d', '!a && !c && !d || !b && !c && !d');136testNegate('!a && !b || !c && !d', 'a && c || a && d || b && c || b && d');137testNegate('!a && !b || !c && !d || !e && !f', 'a && c && e || a && c && f || a && d && e || a && d && f || b && c && e || b && c && f || b && d && e || b && d && f');138});139140test('false, true', () => {141function testNormalize(expr: string, expected: string): void {142const actual = ContextKeyExpr.deserialize(expr)!.serialize();143assert.strictEqual(actual, expected);144}145testNormalize('true', 'true');146testNormalize('!true', 'false');147testNormalize('false', 'false');148testNormalize('!false', 'true');149testNormalize('a && true', 'a');150testNormalize('a && false', 'false');151testNormalize('a || true', 'true');152testNormalize('a || false', 'a');153testNormalize('isMac', isMacintosh ? 'true' : 'false');154testNormalize('isLinux', isLinux ? 'true' : 'false');155testNormalize('isWindows', isWindows ? 'true' : 'false');156});157158test('issue #101015: distribute OR', () => {159function t(expr1: string, expr2: string, expected: string | undefined): void {160const e1 = ContextKeyExpr.deserialize(expr1);161const e2 = ContextKeyExpr.deserialize(expr2);162const actual = ContextKeyExpr.and(e1, e2)?.serialize();163assert.strictEqual(actual, expected);164}165t('a', 'b', 'a && b');166t('a || b', 'c', 'a && c || b && c');167t('a || b', 'c || d', 'a && c || a && d || b && c || b && d');168t('a || b', 'c && d', 'a && c && d || b && c && d');169t('a || b', 'c && d || e', 'a && e || b && e || a && c && d || b && c && d');170});171172test('ContextKeyInExpr', () => {173const ainb = ContextKeyExpr.deserialize('a in b')!;174assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true);175assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true);176assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false);177assert.strictEqual(ainb.evaluate(createContext({ 'a': 3 })), false);178assert.strictEqual(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false);179assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true);180assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false);181assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false);182assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true);183assert.strictEqual(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true);184assert.strictEqual(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false);185});186187test('ContextKeyNotInExpr', () => {188const aNotInB = ContextKeyExpr.deserialize('a not in b')!;189assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), false);190assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), false);191assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), true);192assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 3 })), true);193assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 3, 'b': null })), true);194assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), false);195assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), true);196assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 'x', 'b': {} })), true);197assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), false);198assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), false);199assert.strictEqual(aNotInB.evaluate(createContext({ 'a': 'prototype', 'b': {} })), true);200});201202test('issue #106524: distributing AND should normalize', () => {203const actual = ContextKeyExpr.and(204ContextKeyExpr.or(205ContextKeyExpr.has('a'),206ContextKeyExpr.has('b')207),208ContextKeyExpr.has('c')209);210const expected = ContextKeyExpr.or(211ContextKeyExpr.and(212ContextKeyExpr.has('a'),213ContextKeyExpr.has('c')214),215ContextKeyExpr.and(216ContextKeyExpr.has('b'),217ContextKeyExpr.has('c')218)219);220assert.strictEqual(actual!.equals(expected!), true);221});222223test('issue #129625: Removes duplicated terms in OR expressions', () => {224const expr = ContextKeyExpr.or(225ContextKeyExpr.has('A'),226ContextKeyExpr.has('B'),227ContextKeyExpr.has('A')228)!;229assert.strictEqual(expr.serialize(), 'A || B');230});231232test('Resolves true constant OR expressions', () => {233const expr = ContextKeyExpr.or(234ContextKeyExpr.has('A'),235ContextKeyExpr.not('A')236)!;237assert.strictEqual(expr.serialize(), 'true');238});239240test('Resolves false constant AND expressions', () => {241const expr = ContextKeyExpr.and(242ContextKeyExpr.has('A'),243ContextKeyExpr.not('A')244)!;245assert.strictEqual(expr.serialize(), 'false');246});247248test('issue #129625: Removes duplicated terms in AND expressions', () => {249const expr = ContextKeyExpr.and(250ContextKeyExpr.has('A'),251ContextKeyExpr.has('B'),252ContextKeyExpr.has('A')253)!;254assert.strictEqual(expr.serialize(), 'A && B');255});256257test('issue #129625: Remove duplicated terms when negating', () => {258const expr = ContextKeyExpr.and(259ContextKeyExpr.has('A'),260ContextKeyExpr.or(261ContextKeyExpr.has('B1'),262ContextKeyExpr.has('B2'),263)264)!;265assert.strictEqual(expr.serialize(), 'A && B1 || A && B2');266assert.strictEqual(expr.negate()!.serialize(), '!A || !A && !B1 || !A && !B2 || !B1 && !B2');267assert.strictEqual(expr.negate()!.negate()!.serialize(), 'A && B1 || A && B2');268assert.strictEqual(expr.negate()!.negate()!.negate()!.serialize(), '!A || !A && !B1 || !A && !B2 || !B1 && !B2');269});270271test('issue #129625: remove redundant terms in OR expressions', () => {272function strImplies(p0: string, q0: string): boolean {273const p = ContextKeyExpr.deserialize(p0)!;274const q = ContextKeyExpr.deserialize(q0)!;275return implies(p, q);276}277assert.strictEqual(strImplies('a && b', 'a'), true);278assert.strictEqual(strImplies('a', 'a && b'), false);279});280281test('implies', () => {282function strImplies(p0: string, q0: string): boolean {283const p = ContextKeyExpr.deserialize(p0)!;284const q = ContextKeyExpr.deserialize(q0)!;285return implies(p, q);286}287assert.strictEqual(strImplies('a', 'a'), true);288assert.strictEqual(strImplies('a', 'a || b'), true);289assert.strictEqual(strImplies('a', 'a && b'), false);290assert.strictEqual(strImplies('a', 'a && b || a && c'), false);291assert.strictEqual(strImplies('a && b', 'a'), true);292assert.strictEqual(strImplies('a && b', 'b'), true);293assert.strictEqual(strImplies('a && b', 'a && b || c'), true);294assert.strictEqual(strImplies('a || b', 'a || c'), false);295assert.strictEqual(strImplies('a || b', 'a || b'), true);296assert.strictEqual(strImplies('a && b', 'a && b'), true);297assert.strictEqual(strImplies('a || b', 'a || b || c'), true);298assert.strictEqual(strImplies('c && a && b', 'c && a'), true);299});300301test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => {302function checkEvaluate(expr: string, ctx: any, expected: any): void {303const _expr = ContextKeyExpr.deserialize(expr)!;304assert.strictEqual(_expr.evaluate(createContext(ctx)), expected);305}306307checkEvaluate('a > 1', {}, false);308checkEvaluate('a > 1', { a: 0 }, false);309checkEvaluate('a > 1', { a: 1 }, false);310checkEvaluate('a > 1', { a: 2 }, true);311checkEvaluate('a > 1', { a: '0' }, false);312checkEvaluate('a > 1', { a: '1' }, false);313checkEvaluate('a > 1', { a: '2' }, true);314checkEvaluate('a > 1', { a: 'a' }, false);315316checkEvaluate('a > 10', { a: 2 }, false);317checkEvaluate('a > 10', { a: 11 }, true);318checkEvaluate('a > 10', { a: '11' }, true);319checkEvaluate('a > 10', { a: '2' }, false);320checkEvaluate('a > 10', { a: '11' }, true);321322checkEvaluate('a > 1.1', { a: 1 }, false);323checkEvaluate('a > 1.1', { a: 2 }, true);324checkEvaluate('a > 1.1', { a: 11 }, true);325checkEvaluate('a > 1.1', { a: '1.1' }, false);326checkEvaluate('a > 1.1', { a: '2' }, true);327checkEvaluate('a > 1.1', { a: '11' }, true);328329checkEvaluate('a > b', { a: 'b' }, false);330checkEvaluate('a > b', { a: 'c' }, false);331checkEvaluate('a > b', { a: 1000 }, false);332333checkEvaluate('a >= 2', { a: '1' }, false);334checkEvaluate('a >= 2', { a: '2' }, true);335checkEvaluate('a >= 2', { a: '3' }, true);336337checkEvaluate('a < 2', { a: '1' }, true);338checkEvaluate('a < 2', { a: '2' }, false);339checkEvaluate('a < 2', { a: '3' }, false);340341checkEvaluate('a <= 2', { a: '1' }, true);342checkEvaluate('a <= 2', { a: '2' }, true);343checkEvaluate('a <= 2', { a: '3' }, false);344});345346test('Greater, GreaterEquals, Smaller, SmallerEquals negate', () => {347function checkNegate(expr: string, expected: string): void {348const a = ContextKeyExpr.deserialize(expr)!;349const b = a.negate();350assert.strictEqual(b.serialize(), expected);351}352353checkNegate('a > 1', 'a <= 1');354checkNegate('a > 1.1', 'a <= 1.1');355checkNegate('a > b', 'a <= b');356357checkNegate('a >= 1', 'a < 1');358checkNegate('a >= 1.1', 'a < 1.1');359checkNegate('a >= b', 'a < b');360361checkNegate('a < 1', 'a >= 1');362checkNegate('a < 1.1', 'a >= 1.1');363checkNegate('a < b', 'a >= b');364365checkNegate('a <= 1', 'a > 1');366checkNegate('a <= 1.1', 'a > 1.1');367checkNegate('a <= b', 'a > b');368});369370test('issue #111899: context keys can use `<` or `>` ', () => {371const actual = ContextKeyExpr.deserialize('editorTextFocus && vim.active && vim.use<C-r>')!;372assert.ok(actual.equals(373ContextKeyExpr.and(374ContextKeyExpr.has('editorTextFocus'),375ContextKeyExpr.has('vim.active'),376ContextKeyExpr.has('vim.use<C-r>'),377)!378));379});380});381382383