From fcf099b7801c1193147a6989a3ea4865608bc56a Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Mon, 2 Oct 2023 15:12:30 -0400 Subject: [PATCH 01/24] Initial @woocommerce/expression-evaluation package --- .../js/expression-evaluation/.eslintrc.js | 4 + packages/js/expression-evaluation/.npmrc | 1 + .../js/expression-evaluation/CHANGELOG.md | 3 + packages/js/expression-evaluation/README.md | 1 + .../js/expression-evaluation/babel.config.js | 3 + .../js/expression-evaluation/composer.json | 32 ++ .../js/expression-evaluation/composer.lock | 483 ++++++++++++++++++ .../js/expression-evaluation/jest.config.json | 4 + .../js/expression-evaluation/package.json | 69 +++ .../expression-evaluation/tsconfig-cjs.json | 11 + .../js/expression-evaluation/tsconfig.json | 14 + 11 files changed, 625 insertions(+) create mode 100644 packages/js/expression-evaluation/.eslintrc.js create mode 100644 packages/js/expression-evaluation/.npmrc create mode 100644 packages/js/expression-evaluation/CHANGELOG.md create mode 100644 packages/js/expression-evaluation/README.md create mode 100644 packages/js/expression-evaluation/babel.config.js create mode 100644 packages/js/expression-evaluation/composer.json create mode 100644 packages/js/expression-evaluation/composer.lock create mode 100644 packages/js/expression-evaluation/jest.config.json create mode 100644 packages/js/expression-evaluation/package.json create mode 100644 packages/js/expression-evaluation/tsconfig-cjs.json create mode 100644 packages/js/expression-evaluation/tsconfig.json diff --git a/packages/js/expression-evaluation/.eslintrc.js b/packages/js/expression-evaluation/.eslintrc.js new file mode 100644 index 00000000000..e4d185d8cd1 --- /dev/null +++ b/packages/js/expression-evaluation/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ], + root: true, +}; diff --git a/packages/js/expression-evaluation/.npmrc b/packages/js/expression-evaluation/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/packages/js/expression-evaluation/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/js/expression-evaluation/CHANGELOG.md b/packages/js/expression-evaluation/CHANGELOG.md new file mode 100644 index 00000000000..f7aac6be3b3 --- /dev/null +++ b/packages/js/expression-evaluation/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/packages/js/expression-evaluation/README.md b/packages/js/expression-evaluation/README.md new file mode 100644 index 00000000000..0fff47727d1 --- /dev/null +++ b/packages/js/expression-evaluation/README.md @@ -0,0 +1 @@ +# @woocommerce/expression-evaluation diff --git a/packages/js/expression-evaluation/babel.config.js b/packages/js/expression-evaluation/babel.config.js new file mode 100644 index 00000000000..f73e04467aa --- /dev/null +++ b/packages/js/expression-evaluation/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: '../internal-js-tests/babel.config.js', +}; diff --git a/packages/js/expression-evaluation/composer.json b/packages/js/expression-evaluation/composer.json new file mode 100644 index 00000000000..b7e276fcbea --- /dev/null +++ b/packages/js/expression-evaluation/composer.json @@ -0,0 +1,32 @@ +{ + "name": "woocommerce/expression-evaluation", + "description": "WooCommerce expression evaluation library", + "type": "library", + "license": "GPL-3.0-or-later", + "minimum-stability": "dev", + "require-dev": { + "automattic/jetpack-changelogger": "3.3.0" + }, + "config": { + "platform": { + "php": "7.2" + } + }, + "extra": { + "changelogger": { + "formatter": { + "filename": "../../../tools/changelogger/class-package-formatter.php" + }, + "types": { + "fix": "Fixes an existing bug", + "add": "Adds functionality", + "update": "Update existing functionality", + "dev": "Development related task", + "tweak": "A minor adjustment to the codebase", + "performance": "Address performance issues", + "enhancement": "Improve existing functionality" + }, + "changelog": "CHANGELOG.md" + } + } +} diff --git a/packages/js/expression-evaluation/composer.lock b/packages/js/expression-evaluation/composer.lock new file mode 100644 index 00000000000..0448aa63f09 --- /dev/null +++ b/packages/js/expression-evaluation/composer.lock @@ -0,0 +1,483 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "76226c5737d8666789d8a162710469da", + "packages": [], + "packages-dev": [ + { + "name": "automattic/jetpack-changelogger", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-changelogger.git", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/console": "^3.4 || ^5.2 || ^6.0", + "symfony/process": "^3.4 || ^5.2 || ^6.0", + "wikimedia/at-ease": "^1.2 || ^2.0" + }, + "require-dev": { + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "yoast/phpunit-polyfills": "1.0.4" + }, + "bin": [ + "bin/changelogger" + ], + "type": "project", + "extra": { + "autotagger": true, + "branch-alias": { + "dev-trunk": "3.3.x-dev" + }, + "mirror-repo": "Automattic/jetpack-changelogger", + "version-constants": { + "::VERSION": "src/Application.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}" + } + }, + "autoload": { + "psr-4": { + "Automattic\\Jetpack\\Changelog\\": "lib", + "Automattic\\Jetpack\\Changelogger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.", + "support": { + "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0" + }, + "time": "2022-12-26T13:49:01+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "symfony/console", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "symfony/debug", + "version": "4.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", + "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/debug/tree/v4.4.44" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "abandoned": "symfony/error-handler", + "time": "2022-07-28T16:29:46+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/process", + "version": "3.4.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "wikimedia/at-ease", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/at-ease.git", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a", + "reference": "013ac61929797839c80a111a3f1a4710d8248e7a", + "shasum": "" + }, + "require": { + "php": ">=5.6.99" + }, + "require-dev": { + "jakub-onderka/php-console-highlighter": "0.3.2", + "jakub-onderka/php-parallel-lint": "1.0.0", + "mediawiki/mediawiki-codesniffer": "22.0.0", + "mediawiki/minus-x": "0.3.1", + "ockcyp/covers-validator": "0.5.1 || 0.6.1", + "phpunit/phpunit": "4.8.36 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Wikimedia/Functions.php" + ], + "psr-4": { + "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Tim Starling", + "email": "tstarling@wikimedia.org" + }, + { + "name": "MediaWiki developers", + "email": "wikitech-l@lists.wikimedia.org" + } + ], + "description": "Safe replacement to @ for suppressing warnings.", + "homepage": "https://www.mediawiki.org/wiki/at-ease", + "support": { + "source": "https://github.com/wikimedia/at-ease/tree/master" + }, + "time": "2018-10-10T15:39:06+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "platform-overrides": { + "php": "7.2" + }, + "plugin-api-version": "2.6.0" +} diff --git a/packages/js/expression-evaluation/jest.config.json b/packages/js/expression-evaluation/jest.config.json new file mode 100644 index 00000000000..3d8108048f6 --- /dev/null +++ b/packages/js/expression-evaluation/jest.config.json @@ -0,0 +1,4 @@ +{ + "rootDir": "./src", + "preset": "../node_modules/@woocommerce/internal-js-tests/jest-preset.js" +} diff --git a/packages/js/expression-evaluation/package.json b/packages/js/expression-evaluation/package.json new file mode 100644 index 00000000000..949a3b3cba7 --- /dev/null +++ b/packages/js/expression-evaluation/package.json @@ -0,0 +1,69 @@ +{ + "name": "@woocommerce/expression-evaluation", + "version": "0.0.1", + "description": "Library for evaluating expressions.", + "author": "Automattic", + "license": "GPL-3.0-or-later", + "keywords": [ + "wordpress", + "woocommerce", + "expression", + "evalution" + ], + "engines": { + "node": "^16.14.1", + "pnpm": "^8.6.7" + }, + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/expression-evaluation/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "bugs": { + "url": "https://github.com/woocommerce/woocommerce/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "types": "build-types", + "react-native": "src/index", + "dependencies": { + "@wordpress/i18n": "wp-6.0", + "peggy": "^3.0.2" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "turbo:build": "tsc --project tsconfig.json && tsc --project tsconfig-cjs.json", + "turbo:test": "jest --config ./jest.config.json", + "prepare": "composer install", + "changelog": "composer exec -- changelogger", + "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", + "build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name", + "test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name", + "lint": "eslint --output-file eslint_report.json --format json src", + "start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\"", + "prepack": "pnpm run clean && pnpm run build", + "lint:fix": "eslint src --fix", + "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "devDependencies": { + "@babel/core": "^7.17.5", + "@types/jest": "^27.4.1", + "@woocommerce/eslint-plugin": "workspace:*", + "@woocommerce/internal-js-tests": "workspace:*", + "concurrently": "^7.0.0", + "eslint": "^8.32.0", + "jest": "^27.5.1", + "jest-cli": "^27.5.1", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.3", + "typescript": "^5.1.6" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "pnpm lint:fix", + "pnpm test-staged" + ] + } +} diff --git a/packages/js/expression-evaluation/tsconfig-cjs.json b/packages/js/expression-evaluation/tsconfig-cjs.json new file mode 100644 index 00000000000..61782c90442 --- /dev/null +++ b/packages/js/expression-evaluation/tsconfig-cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig-cjs", + "compilerOptions": { + "declaration": true, + "outDir": "build", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + } +} diff --git a/packages/js/expression-evaluation/tsconfig.json b/packages/js/expression-evaluation/tsconfig.json new file mode 100644 index 00000000000..c5f351a60cc --- /dev/null +++ b/packages/js/expression-evaluation/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "build-module", + "declaration": true, + "declarationMap": true, + "declarationDir": "./build-types", + "typeRoots": [ + "./typings", + "./node_modules/@types" + ] + } +} From 0f55dff6def9e6cdea92120602f587f2b70d4e9d Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Mon, 2 Oct 2023 15:12:44 -0400 Subject: [PATCH 02/24] Initial grammar --- .../js/expression-evaluation/src/index.ts | 0 .../js/expression-evaluation/src/parser.ts | 52 +++++++++++++ .../src/test/parser.test.ts | 75 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 packages/js/expression-evaluation/src/index.ts create mode 100644 packages/js/expression-evaluation/src/parser.ts create mode 100644 packages/js/expression-evaluation/src/test/parser.test.ts diff --git a/packages/js/expression-evaluation/src/index.ts b/packages/js/expression-evaluation/src/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts new file mode 100644 index 00000000000..96f9f80c31a --- /dev/null +++ b/packages/js/expression-evaluation/src/parser.ts @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import * as peggy from 'peggy'; + +const grammar = ` +start = logicalOr + +logicalOr + = left:logicalAnd whitespace+ "||" whitespace+ right:logicalOr { + return left || right; + } + / logicalAnd + +logicalAnd + = left:primary whitespace+ "&&" whitespace+ right:logicalAnd { + return left && right; + } + / factor + +factor + = "!" whitespace* operand:factor { + return !operand; + } + / primary + +primary + = variable + / "(" logicalOr:logicalOr ")" { + return logicalOr; + } + +variable + = variable:identifier accessor:("." identifier)* { + const path = variable.split( '.' ); + let result = path.reduce( ( nextObject, propertyName ) => nextObject[ propertyName ], options.context ); + + for ( let i = 0; i < accessor.length; i++ ) { + result = result[ accessor[ i ][ 1 ] ]; + } + + return result; + } + +identifier + = identifier:$[a-zA-Z0-9_]+ + +whitespace + = [ \t] +`; + +export const parser = peggy.generate( grammar ); diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts new file mode 100644 index 00000000000..5e7627dc041 --- /dev/null +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ + +import { parser } from '../parser'; + +describe( 'parser', () => { + it( 'should parse a top-level context property', () => { + const result = parser.parse( 'foo', { + context: { + foo: 'bar', + }, + } ); + + expect( result ).toEqual( 'bar' ); + } ); + + it( 'should parse a nested context property', () => { + const result = parser.parse( 'foo.bar', { + context: { + foo: { + bar: 'baz', + }, + }, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should parse a logical OR expression', () => { + const result = parser.parse( 'foo || bar', { + context: { + foo: true, + bar: false, + }, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should parse a logical AND expression', () => { + const result = parser.parse( 'foo && bar', { + context: { + foo: true, + bar: false, + }, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should parse a NOT expression', () => { + const result = parser.parse( '!foo', { + context: { + foo: true, + }, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should parse a double NOT expression', () => { + const result = parser.parse( '!!foo', { + context: { + foo: true, + }, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should throw an error if the expression is invalid', () => { + expect( () => parser.parse( '1' ) ).toThrow(); + } ); +} ); From f6ee58a4cef3371c85bd8c7c1ebfb884afc7a3bb Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Mon, 2 Oct 2023 19:58:02 -0400 Subject: [PATCH 03/24] Change case of grammar rules --- .../js/expression-evaluation/src/parser.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 96f9f80c31a..b6e369b09f3 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -4,34 +4,34 @@ import * as peggy from 'peggy'; const grammar = ` -start = logicalOr +Start = LogicalOr -logicalOr - = left:logicalAnd whitespace+ "||" whitespace+ right:logicalOr { +LogicalOr + = left:LogicalAnd WhiteSpace+ "||" WhiteSpace+ right:LogicalOr { return left || right; } - / logicalAnd + / LogicalAnd -logicalAnd - = left:primary whitespace+ "&&" whitespace+ right:logicalAnd { +LogicalAnd + = left:Primary WhiteSpace+ "&&" WhiteSpace+ right:LogicalAnd { return left && right; } - / factor + / Factor -factor - = "!" whitespace* operand:factor { +Factor + = "!" WhiteSpace* operand:Factor { return !operand; } - / primary + / Primary -primary - = variable - / "(" logicalOr:logicalOr ")" { +Primary + = Variable + / "(" logicalOr:LogicalOr ")" { return logicalOr; } -variable - = variable:identifier accessor:("." identifier)* { +Variable + = variable:Identifier accessor:("." Identifier)* { const path = variable.split( '.' ); let result = path.reduce( ( nextObject, propertyName ) => nextObject[ propertyName ], options.context ); @@ -42,11 +42,12 @@ variable return result; } -identifier +Identifier = identifier:$[a-zA-Z0-9_]+ -whitespace - = [ \t] +WhiteSpace + = " " + / "\t" `; export const parser = peggy.generate( grammar ); From 32964d8556c1f32e4d40daca460768f22ced0ae4 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Mon, 2 Oct 2023 20:50:59 -0400 Subject: [PATCH 04/24] Null, boolean, and numeric literals --- .../js/expression-evaluation/src/parser.ts | 152 ++++++++++++++---- .../src/test/parser.test.ts | 50 +++++- 2 files changed, 172 insertions(+), 30 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index b6e369b09f3..c6e007429fb 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -4,33 +4,14 @@ import * as peggy from 'peggy'; const grammar = ` -Start = LogicalOr +Start + = LogicalOr -LogicalOr - = left:LogicalAnd WhiteSpace+ "||" WhiteSpace+ right:LogicalOr { - return left || right; - } - / LogicalAnd +WhiteSpace + = " " + / "\t" -LogicalAnd - = left:Primary WhiteSpace+ "&&" WhiteSpace+ right:LogicalAnd { - return left && right; - } - / Factor - -Factor - = "!" WhiteSpace* operand:Factor { - return !operand; - } - / Primary - -Primary - = Variable - / "(" logicalOr:LogicalOr ")" { - return logicalOr; - } - -Variable +IdentifierPath = variable:Identifier accessor:("." Identifier)* { const path = variable.split( '.' ); let result = path.reduce( ( nextObject, propertyName ) => nextObject[ propertyName ], options.context ); @@ -43,11 +24,124 @@ Variable } Identifier - = identifier:$[a-zA-Z0-9_]+ + = !ReservedWord name:IdentifierName { + return name; + } -WhiteSpace - = " " - / "\t" +IdentifierName + = first:IdentifierStart rest:IdentifierPart* { + return first + rest.join( '' ); + } + +IdentifierStart + = [a-zA-Z] + / "_" + / "$" + +IdentifierPart + = IdentifierStart + +ReservedWord + = NullLiteral + / BooleanLiteral + +// Literals + +Literal + = NullLiteral + / BooleanLiteral + / NumericLiteral + +NullLiteral + = NullToken { return null; } + +BooleanLiteral + = "true" { return true; } + / "false" { return false; } + +NumericLiteral + = literal:HexIntegerLiteral !(IdentifierStart / DecimalDigit) { + return literal; + } + / literal:DecimalLiteral !(IdentifierStart / DecimalDigit) { + return literal; + } + +HexIntegerLiteral + = "0x"i digits:$HexDigit+ { + return parseInt( digits, 16 ); + } + +HexDigit + = [0-9a-f]i + +DecimalLiteral + = DecimalIntegerLiteral "." DecimalDigit* ExponentPart? { + return parseFloat( text() ); + } + / "." DecimalDigit+ ExponentPart? { + return parseFloat( text() ); + } + / DecimalIntegerLiteral ExponentPart? { + return parseFloat( text() ); + } + +DecimalIntegerLiteral + = "0" + / NonZeroDigit DecimalDigit* + +DecimalDigit + = [0-9] + +NonZeroDigit + = [1-9] + +ExponentPart + = ExponentIndicator SignedInteger + +ExponentIndicator + = "e"i + +SignedInteger + = [+-]? DecimalDigit+ + +// Tokens + +NullToken + = "null" !IdentifierPart + +TrueToken + = "true" !IdentifierPart + +FalseToken + = "false" !IdentifierPart + +// Logical Expressions + +LogicalOr + = left:LogicalAnd WhiteSpace+ "||" WhiteSpace+ right:LogicalOr { + return left || right; + } + / LogicalAnd + +LogicalAnd + = left:PrimaryExpression WhiteSpace+ "&&" WhiteSpace+ right:LogicalAnd { + return left && right; + } + / Factor + +Factor + = "!" WhiteSpace* operand:Factor { + return !operand; + } + / PrimaryExpression + +PrimaryExpression + = IdentifierPath + / Literal + / "(" WhiteSpace* expression:LogicalOr WhiteSpace* ")" { + return expression; + } `; export const parser = peggy.generate( grammar ); diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index 5e7627dc041..1ebb246ddc3 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -5,6 +5,54 @@ import { parser } from '../parser'; describe( 'parser', () => { + it( 'should parse a null literal', () => { + const result = parser.parse( 'null' ); + + expect( result ).toEqual( null ); + } ); + + it( 'should parse a boolean true literal', () => { + const result = parser.parse( 'true' ); + + expect( result ).toEqual( true ); + } ); + + it( 'should parse a boolean false literal', () => { + const result = parser.parse( 'false' ); + + expect( result ).toEqual( false ); + } ); + + it( 'should parse a numeric integer literal', () => { + const result = parser.parse( '23' ); + + expect( result ).toEqual( 23 ); + } ); + + it( 'should parse a numeric floating point literal', () => { + const result = parser.parse( '5.23' ); + + expect( result ).toEqual( 5.23 ); + } ); + + it( 'should parse a numeric hexadecimal literal', () => { + const result = parser.parse( '0x23' ); + + expect( result ).toEqual( 35 ); + } ); + + it( 'should parse a string literal with double quotes', () => { + const result = parser.parse( '"foo"' ); + + expect( result ).toEqual( 'foo' ); + } ); + + it( 'should parse a string literal with single quotes', () => { + const result = parser.parse( "'foo'" ); + + expect( result ).toEqual( 'foo' ); + } ); + it( 'should parse a top-level context property', () => { const result = parser.parse( 'foo', { context: { @@ -70,6 +118,6 @@ describe( 'parser', () => { } ); it( 'should throw an error if the expression is invalid', () => { - expect( () => parser.parse( '1' ) ).toThrow(); + expect( () => parser.parse( '= 1' ) ).toThrow(); } ); } ); From e9180206b07dea8181d415e7de4da96e3ae197ce Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 13:56:45 -0400 Subject: [PATCH 05/24] String literals --- .../js/expression-evaluation/src/parser.ts | 106 +++++++++++++++++- .../src/test/parser.test.ts | 39 +++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index c6e007429fb..9071f06336d 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -7,9 +7,25 @@ const grammar = ` Start = LogicalOr +SourceCharacter + = . + WhiteSpace = " " - / "\t" + / "\\t" + +LineTerminator + = "\\n" + / "\\r" + / "\\u2028" + / "\\u2029" + +LineTerminatorSequence + = "\\n" + / "\\r\\n" + / "\\r" + / "\\u2028" + / "\\u2029" IdentifierPath = variable:Identifier accessor:("." Identifier)* { @@ -51,6 +67,7 @@ Literal = NullLiteral / BooleanLiteral / NumericLiteral + / StringLiteral NullLiteral = NullToken { return null; } @@ -105,6 +122,93 @@ ExponentIndicator SignedInteger = [+-]? DecimalDigit+ +StringLiteral + = '"' chars:DoubleQuotedStringCharacter* '"' { + return chars.join( '' ); + } + / "'" chars:SingleQuotedStringCharacter* "'" { + return chars.join( '' ); + } + +DoubleQuotedStringCharacter + = !('"' / "\\\\" / LineTerminator) SourceCharacter { + return text(); + } + / "\\\\" escapeSequence:EscapeSequence { + return escapeSequence; + } + / LineContinuation + +SingleQuotedStringCharacter + = !("'" / "\\\\" / LineTerminator) SourceCharacter { + return text(); + } + / "\\\\" escapeSequence:EscapeSequence { + return escapeSequence; + } + / LineContinuation + +LineContinuation + = "\\\\" LineTerminatorSequence { + return ''; + } + +EscapeSequence + = CharacterEscapeSequence + / "0" !DecimalDigit { + return "\\0"; + } + / HexEscapeSequence + / UnicodeEscapeSequence + +CharacterEscapeSequence + = SingleEscapeCharacter + / NonEscapeCharacter + +SingleEscapeCharacter + = "'" + / '"' + / "\\\\" + / "b" { + return "\\b"; + } + / "f" { + return "\\f"; + } + / "n" { + return "\\n"; + } + / "r" { + return "\\r"; + } + / "t" { + return "\\t"; + } + / "v" { + return "\\v"; + } + +NonEscapeCharacter + = (!EscapeCharacter / LineTerminator) SourceCharacter { + return text(); + } + +EscapeCharacter + = SingleEscapeCharacter + / DecimalDigit + / "x" + / "u" + +HexEscapeSequence + = "x" digits:$(HexDigit HexDigit) { + return String.fromCharCode( parseInt( digits, 16 ) ); + } + +UnicodeEscapeSequence + = "u" digits:$(HexDigit HexDigit HexDigit HexDigit) { + return String.fromCharCode( parseInt( digits, 16 ) ); + } + // Tokens NullToken diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index 1ebb246ddc3..ffe3738ad2a 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -47,12 +47,51 @@ describe( 'parser', () => { expect( result ).toEqual( 'foo' ); } ); + it( 'should parse a string literal with double quotes and single quotes', () => { + const result = parser.parse( '"foo \'bar\'"' ); + + expect( result ).toEqual( "foo 'bar'" ); + } ); + + it( 'should parse a string literal with double quotes and escaped double quotes', () => { + const result = parser.parse( '"foo \\"bar\\""' ); + + expect( result ).toEqual( 'foo "bar"' ); + } ); + + it( 'should parse a string literal with double quotes and escaped backslashes', () => { + // eslint-disable-next-line prettier/prettier + const result = parser.parse( '"foo \\\\\\"bar\\\\\\""' ); + + expect( result ).toEqual( 'foo \\"bar\\"' ); + } ); + it( 'should parse a string literal with single quotes', () => { const result = parser.parse( "'foo'" ); expect( result ).toEqual( 'foo' ); } ); + it( 'should parse a string literal with single quotes and double quotes', () => { + // eslint-disable-next-line prettier/prettier + const result = parser.parse( "'foo \"bar\"'" ); + + expect( result ).toEqual( 'foo "bar"' ); + } ); + + it( 'should parse a string literal with single quotes and escaped single quotes', () => { + const result = parser.parse( "'foo \\'bar\\''" ); + + expect( result ).toEqual( "foo 'bar'" ); + } ); + + it( 'should parse a string literal with single quotes and escaped backslashes', () => { + // eslint-disable-next-line prettier/prettier + const result = parser.parse( "'foo \\\\\\'bar\\\\\\''" ); + + expect( result ).toEqual( "foo \\'bar\\'" ); + } ); + it( 'should parse a top-level context property', () => { const result = parser.parse( 'foo', { context: { From a5fec1fa6e6f89d2d1bb71f21537ab0f2164cb47 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 15:43:40 -0400 Subject: [PATCH 06/24] Extract OR and AND operators --- .../js/expression-evaluation/src/parser.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 9071f06336d..1ec0e77e16f 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -5,7 +5,7 @@ import * as peggy from 'peggy'; const grammar = ` Start - = LogicalOr + = LogicalOrExpression SourceCharacter = . @@ -220,32 +220,40 @@ TrueToken FalseToken = "false" !IdentifierPart -// Logical Expressions +// Expressions -LogicalOr - = left:LogicalAnd WhiteSpace+ "||" WhiteSpace+ right:LogicalOr { +PrimaryExpression + = IdentifierPath + / Literal + / "(" WhiteSpace* expression:LogicalOrExpression WhiteSpace* ")" { + return expression; + } + +LogicalOrExpression + = left:LogicalAndExpression WhiteSpace+ LogicalOrOperator WhiteSpace+ right:LogicalOrExpression { return left || right; } - / LogicalAnd + / LogicalAndExpression -LogicalAnd - = left:PrimaryExpression WhiteSpace+ "&&" WhiteSpace+ right:LogicalAnd { +LogicalOrOperator + = "||" + +LogicalAndExpression + = left:PrimaryExpression WhiteSpace+ LogicalAndOperator WhiteSpace+ right:LogicalAndExpression { return left && right; } / Factor +LogicalAndOperator + = "&&" + Factor = "!" WhiteSpace* operand:Factor { return !operand; } / PrimaryExpression -PrimaryExpression - = IdentifierPath - / Literal - / "(" WhiteSpace* expression:LogicalOr WhiteSpace* ")" { - return expression; - } + `; export const parser = peggy.generate( grammar ); From 6237c8caea12238e4b8723d179bd8598b8ac8188 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 19:04:46 -0400 Subject: [PATCH 07/24] Refactor logical expressions and implement equality expressions --- .../js/expression-evaluation/src/parser.ts | 70 +++++++++++++++---- .../src/test/parser.test.ts | 40 +++++++++++ 2 files changed, 98 insertions(+), 12 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 1ec0e77e16f..f53fb416045 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -4,8 +4,41 @@ import * as peggy from 'peggy'; const grammar = ` +{{ + function evaluateBinaryExpression( head, tail ) { + return tail.reduce( ( leftOperand, tailElement ) => { + const operator = tailElement[ 1 ]; + const rightOperand = tailElement[ 3 ]; + + switch ( operator ) { + case '&&': + return leftOperand && rightOperand; + break; + case '||': + return leftOperand || rightOperand; + break; + case '===': + return leftOperand === rightOperand; + break; + case '!==': + return leftOperand !== rightOperand; + break; + case '==': + return leftOperand == rightOperand; + break; + case '!=': + return leftOperand != rightOperand; + break; + default: + return undefined; + break; + } + }, head ); + } +}} + Start - = LogicalOrExpression + = Expression SourceCharacter = . @@ -225,24 +258,28 @@ FalseToken PrimaryExpression = IdentifierPath / Literal - / "(" WhiteSpace* expression:LogicalOrExpression WhiteSpace* ")" { + / "(" WhiteSpace* expression:Expression WhiteSpace* ")" { return expression; } -LogicalOrExpression - = left:LogicalAndExpression WhiteSpace+ LogicalOrOperator WhiteSpace+ right:LogicalOrExpression { - return left || right; - } - / LogicalAndExpression +RelationalExpression + = PrimaryExpression -LogicalOrOperator - = "||" +EqualityExpression + = head:RelationalExpression tail:( WhiteSpace* EqualityOperator WhiteSpace* RelationalExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +EqualityOperator + = "===" + / "!==" + / "==" + / "!=" LogicalAndExpression - = left:PrimaryExpression WhiteSpace+ LogicalAndOperator WhiteSpace+ right:LogicalAndExpression { - return left && right; + = head:EqualityExpression tail:(WhiteSpace+ LogicalAndOperator WhiteSpace+ EqualityExpression)* { + return evaluateBinaryExpression( head, tail ); } - / Factor LogicalAndOperator = "&&" @@ -253,7 +290,16 @@ Factor } / PrimaryExpression +LogicalOrExpression + = head:LogicalAndExpression tail:(WhiteSpace+ LogicalOrOperator WhiteSpace+ LogicalAndExpression)* { + return evaluateBinaryExpression( head, tail ); + } +LogicalOrOperator + = "||" + +Expression + = LogicalOrExpression `; export const parser = peggy.generate( grammar ); diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index ffe3738ad2a..1e91e4434b0 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -114,6 +114,46 @@ describe( 'parser', () => { expect( result ).toEqual( 'baz' ); } ); + it( 'should parse an strict equality expression', () => { + const result = parser.parse( 'foo === "bar"', { + context: { + foo: 'bar', + }, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should parse an strict inequality expression', () => { + const result = parser.parse( 'foo !== "bar"', { + context: { + foo: 'bar', + }, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should parse an equality expression', () => { + const result = parser.parse( 'foo == "bar"', { + context: { + foo: 'bar', + }, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should parse an inequality expression', () => { + const result = parser.parse( 'foo != "bar"', { + context: { + foo: 'bar', + }, + } ); + + expect( result ).toEqual( false ); + } ); + it( 'should parse a logical OR expression', () => { const result = parser.parse( 'foo || bar', { context: { From d82737fb81ac78907619b0b65f984efcbe30bbbf Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 19:13:56 -0400 Subject: [PATCH 08/24] Implement unary expressions --- .../js/expression-evaluation/src/parser.ts | 30 ++++++++++++++++++- .../src/test/parser.test.ts | 12 ++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index f53fb416045..8831a65cec9 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -5,6 +5,23 @@ import * as peggy from 'peggy'; const grammar = ` {{ + function evaluateUnaryExpression( operator, operand ) { + switch ( operator ) { + case '!': + return !operand; + break; + case '-': + return -operand; + break; + case '+': + return +operand; + break; + default: + return undefined; + break; + } + } + function evaluateBinaryExpression( head, tail ) { return tail.reduce( ( leftOperand, tailElement ) => { const operator = tailElement[ 1 ]; @@ -262,8 +279,19 @@ PrimaryExpression return expression; } -RelationalExpression +UnaryExpression = PrimaryExpression + / operator:UnaryOperator WhiteSpace* operand:UnaryExpression { + return evaluateUnaryExpression( operator, operand ); + } + +UnaryOperator + = "!" + / "-" + / "+" + +RelationalExpression + = UnaryExpression EqualityExpression = head:RelationalExpression tail:( WhiteSpace* EqualityOperator WhiteSpace* RelationalExpression)* { diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index 1e91e4434b0..9a23dca0daa 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -196,6 +196,18 @@ describe( 'parser', () => { expect( result ).toEqual( true ); } ); + it( 'should parse a negative number', () => { + const result = parser.parse( '-1' ); + + expect( result ).toEqual( -1 ); + } ); + + it( 'should parse a positive number', () => { + const result = parser.parse( '+1' ); + + expect( result ).toEqual( 1 ); + } ); + it( 'should throw an error if the expression is invalid', () => { expect( () => parser.parse( '= 1' ) ).toThrow(); } ); From b51b652ebdec44f4e95b37da7dc8c1dfe28b26de Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 19:23:39 -0400 Subject: [PATCH 09/24] Signed number tests --- .../src/test/parser.test.ts | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index 9a23dca0daa..448ef5a780c 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -29,12 +29,36 @@ describe( 'parser', () => { expect( result ).toEqual( 23 ); } ); + it( 'should parse a signed negative integer literal', () => { + const result = parser.parse( '-1' ); + + expect( result ).toEqual( -1 ); + } ); + + it( 'should parse a signed positive integer literal', () => { + const result = parser.parse( '+1' ); + + expect( result ).toEqual( 1 ); + } ); + it( 'should parse a numeric floating point literal', () => { const result = parser.parse( '5.23' ); expect( result ).toEqual( 5.23 ); } ); + it( 'should parse a signed negative floating point literal', () => { + const result = parser.parse( '-9.95' ); + + expect( result ).toEqual( -9.95 ); + } ); + + it( 'should parse a signed positive floating point literal', () => { + const result = parser.parse( '+9.95' ); + + expect( result ).toEqual( 9.95 ); + } ); + it( 'should parse a numeric hexadecimal literal', () => { const result = parser.parse( '0x23' ); @@ -196,18 +220,6 @@ describe( 'parser', () => { expect( result ).toEqual( true ); } ); - it( 'should parse a negative number', () => { - const result = parser.parse( '-1' ); - - expect( result ).toEqual( -1 ); - } ); - - it( 'should parse a positive number', () => { - const result = parser.parse( '+1' ); - - expect( result ).toEqual( 1 ); - } ); - it( 'should throw an error if the expression is invalid', () => { expect( () => parser.parse( '= 1' ) ).toThrow(); } ); From f331099c6425adf0c40aead3fc884d28efdfdd5d Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 19:39:47 -0400 Subject: [PATCH 10/24] Support multiline expressions --- .../js/expression-evaluation/src/parser.ts | 19 ++++++++----------- .../src/test/parser.test.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 8831a65cec9..a87cfaccb36 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -77,6 +77,9 @@ LineTerminatorSequence / "\\u2028" / "\\u2029" +__ "skipped" + = (WhiteSpace / LineTerminatorSequence )* + IdentifierPath = variable:Identifier accessor:("." Identifier)* { const path = variable.split( '.' ); @@ -275,13 +278,13 @@ FalseToken PrimaryExpression = IdentifierPath / Literal - / "(" WhiteSpace* expression:Expression WhiteSpace* ")" { + / "(" __ expression:Expression __ ")" { return expression; } UnaryExpression = PrimaryExpression - / operator:UnaryOperator WhiteSpace* operand:UnaryExpression { + / operator:UnaryOperator __ operand:UnaryExpression { return evaluateUnaryExpression( operator, operand ); } @@ -294,7 +297,7 @@ RelationalExpression = UnaryExpression EqualityExpression - = head:RelationalExpression tail:( WhiteSpace* EqualityOperator WhiteSpace* RelationalExpression)* { + = head:RelationalExpression tail:(__ EqualityOperator __ RelationalExpression)* { return evaluateBinaryExpression( head, tail ); } @@ -305,21 +308,15 @@ EqualityOperator / "!=" LogicalAndExpression - = head:EqualityExpression tail:(WhiteSpace+ LogicalAndOperator WhiteSpace+ EqualityExpression)* { + = head:EqualityExpression tail:(__ LogicalAndOperator __ EqualityExpression)* { return evaluateBinaryExpression( head, tail ); } LogicalAndOperator = "&&" -Factor - = "!" WhiteSpace* operand:Factor { - return !operand; - } - / PrimaryExpression - LogicalOrExpression - = head:LogicalAndExpression tail:(WhiteSpace+ LogicalOrOperator WhiteSpace+ LogicalAndExpression)* { + = head:LogicalAndExpression tail:(__ LogicalOrOperator __ LogicalAndExpression)* { return evaluateBinaryExpression( head, tail ); } diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts index 448ef5a780c..f2ae65ffeb5 100644 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ b/packages/js/expression-evaluation/src/test/parser.test.ts @@ -220,6 +220,23 @@ describe( 'parser', () => { expect( result ).toEqual( true ); } ); + it( 'should parse a multiline expression', () => { + const result = parser.parse( + `foo + || bar + || baz`, + { + context: { + foo: false, + bar: false, + baz: true, + }, + } + ); + + expect( result ).toEqual( true ); + } ); + it( 'should throw an error if the expression is invalid', () => { expect( () => parser.parse( '= 1' ) ).toThrow(); } ); From f0a6045ca09fba15fd6179fa782e413c77b253de Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 3 Oct 2023 19:45:20 -0400 Subject: [PATCH 11/24] Simplify IdentifierName processing --- packages/js/expression-evaluation/src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index a87cfaccb36..ac71da9beb9 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -99,7 +99,7 @@ Identifier IdentifierName = first:IdentifierStart rest:IdentifierPart* { - return first + rest.join( '' ); + return text(); } IdentifierStart From f47bb0db376e0c8956ac47fdfd493e52b90cdd9c Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 09:25:49 -0400 Subject: [PATCH 12/24] Refactor to evaluate function --- .../js/expression-evaluation/src/index.ts | 9 + .../src/test/index.test.ts | 221 ++++++++++++++++ .../src/test/parser.test.ts | 243 ------------------ 3 files changed, 230 insertions(+), 243 deletions(-) create mode 100644 packages/js/expression-evaluation/src/test/index.test.ts delete mode 100644 packages/js/expression-evaluation/src/test/parser.test.ts diff --git a/packages/js/expression-evaluation/src/index.ts b/packages/js/expression-evaluation/src/index.ts index e69de29bb2d..0d499c72029 100644 --- a/packages/js/expression-evaluation/src/index.ts +++ b/packages/js/expression-evaluation/src/index.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ + +import { parser } from './parser'; + +export function evaluate( expression: string, context = {} ) { + return parser.parse( expression, { context } ); +} diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts new file mode 100644 index 00000000000..066ffa76c4c --- /dev/null +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -0,0 +1,221 @@ +/** + * Internal dependencies + */ + +import { evaluate } from '../'; + +describe( 'evaluate', () => { + it( 'should evaluate a null literal', () => { + const result = evaluate( 'null' ); + + expect( result ).toEqual( null ); + } ); + + it( 'should evaluate a boolean true literal', () => { + const result = evaluate( 'true' ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a boolean false literal', () => { + const result = evaluate( 'false' ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a numeric integer literal', () => { + const result = evaluate( '23' ); + + expect( result ).toEqual( 23 ); + } ); + + it( 'should evaluate a signed negative integer literal', () => { + const result = evaluate( '-1' ); + + expect( result ).toEqual( -1 ); + } ); + + it( 'should evaluate a signed positive integer literal', () => { + const result = evaluate( '+1' ); + + expect( result ).toEqual( 1 ); + } ); + + it( 'should evaluate a numeric floating point literal', () => { + const result = evaluate( '5.23' ); + + expect( result ).toEqual( 5.23 ); + } ); + + it( 'should evaluate a signed negative floating point literal', () => { + const result = evaluate( '-9.95' ); + + expect( result ).toEqual( -9.95 ); + } ); + + it( 'should evaluate a signed positive floating point literal', () => { + const result = evaluate( '+9.95' ); + + expect( result ).toEqual( 9.95 ); + } ); + + it( 'should evaluate a numeric hexadecimal literal', () => { + const result = evaluate( '0x23' ); + + expect( result ).toEqual( 35 ); + } ); + + it( 'should evaluate a string literal with double quotes', () => { + const result = evaluate( '"foo"' ); + + expect( result ).toEqual( 'foo' ); + } ); + + it( 'should evaluate a string literal with double quotes and single quotes', () => { + const result = evaluate( '"foo \'bar\'"' ); + + expect( result ).toEqual( "foo 'bar'" ); + } ); + + it( 'should evaluate a string literal with double quotes and escaped double quotes', () => { + const result = evaluate( '"foo \\"bar\\""' ); + + expect( result ).toEqual( 'foo "bar"' ); + } ); + + it( 'should evaluate a string literal with double quotes and escaped backslashes', () => { + // eslint-disable-next-line prettier/prettier + const result = evaluate( '"foo \\\\\\"bar\\\\\\""' ); + + expect( result ).toEqual( 'foo \\"bar\\"' ); + } ); + + it( 'should evaluate a string literal with single quotes', () => { + const result = evaluate( "'foo'" ); + + expect( result ).toEqual( 'foo' ); + } ); + + it( 'should evaluate a string literal with single quotes and double quotes', () => { + // eslint-disable-next-line prettier/prettier + const result = evaluate( "'foo \"bar\"'" ); + + expect( result ).toEqual( 'foo "bar"' ); + } ); + + it( 'should evaluate a string literal with single quotes and escaped single quotes', () => { + const result = evaluate( "'foo \\'bar\\''" ); + + expect( result ).toEqual( "foo 'bar'" ); + } ); + + it( 'should evaluate a string literal with single quotes and escaped backslashes', () => { + // eslint-disable-next-line prettier/prettier + const result = evaluate( "'foo \\\\\\'bar\\\\\\''" ); + + expect( result ).toEqual( "foo \\'bar\\'" ); + } ); + + it( 'should evaluate a top-level context property', () => { + const result = evaluate( 'foo', { + foo: 'bar', + } ); + + expect( result ).toEqual( 'bar' ); + } ); + + it( 'should evaluate a nested context property', () => { + const result = evaluate( 'foo.bar', { + foo: { + bar: 'baz', + }, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should evaluate an strict equality expression', () => { + const result = evaluate( 'foo === "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate an strict inequality expression', () => { + const result = evaluate( 'foo !== "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate an equality expression', () => { + const result = evaluate( 'foo == "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate an inequality expression', () => { + const result = evaluate( 'foo != "bar"', { + foo: 'bar', + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a logical OR expression', () => { + const result = evaluate( 'foo || bar', { + foo: true, + bar: false, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a logical AND expression', () => { + const result = evaluate( 'foo && bar', { + foo: true, + bar: false, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a NOT expression', () => { + const result = evaluate( '!foo', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a double NOT expression', () => { + const result = evaluate( '!!foo', { + foo: true, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a multiline expression', () => { + const result = evaluate( + `foo + || bar + || baz`, + { + foo: false, + bar: false, + baz: true, + } + ); + + expect( result ).toEqual( true ); + } ); + + it( 'should throw an error if the expression is invalid', () => { + expect( () => evaluate( '= 1' ) ).toThrow(); + } ); +} ); diff --git a/packages/js/expression-evaluation/src/test/parser.test.ts b/packages/js/expression-evaluation/src/test/parser.test.ts deleted file mode 100644 index f2ae65ffeb5..00000000000 --- a/packages/js/expression-evaluation/src/test/parser.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Internal dependencies - */ - -import { parser } from '../parser'; - -describe( 'parser', () => { - it( 'should parse a null literal', () => { - const result = parser.parse( 'null' ); - - expect( result ).toEqual( null ); - } ); - - it( 'should parse a boolean true literal', () => { - const result = parser.parse( 'true' ); - - expect( result ).toEqual( true ); - } ); - - it( 'should parse a boolean false literal', () => { - const result = parser.parse( 'false' ); - - expect( result ).toEqual( false ); - } ); - - it( 'should parse a numeric integer literal', () => { - const result = parser.parse( '23' ); - - expect( result ).toEqual( 23 ); - } ); - - it( 'should parse a signed negative integer literal', () => { - const result = parser.parse( '-1' ); - - expect( result ).toEqual( -1 ); - } ); - - it( 'should parse a signed positive integer literal', () => { - const result = parser.parse( '+1' ); - - expect( result ).toEqual( 1 ); - } ); - - it( 'should parse a numeric floating point literal', () => { - const result = parser.parse( '5.23' ); - - expect( result ).toEqual( 5.23 ); - } ); - - it( 'should parse a signed negative floating point literal', () => { - const result = parser.parse( '-9.95' ); - - expect( result ).toEqual( -9.95 ); - } ); - - it( 'should parse a signed positive floating point literal', () => { - const result = parser.parse( '+9.95' ); - - expect( result ).toEqual( 9.95 ); - } ); - - it( 'should parse a numeric hexadecimal literal', () => { - const result = parser.parse( '0x23' ); - - expect( result ).toEqual( 35 ); - } ); - - it( 'should parse a string literal with double quotes', () => { - const result = parser.parse( '"foo"' ); - - expect( result ).toEqual( 'foo' ); - } ); - - it( 'should parse a string literal with double quotes and single quotes', () => { - const result = parser.parse( '"foo \'bar\'"' ); - - expect( result ).toEqual( "foo 'bar'" ); - } ); - - it( 'should parse a string literal with double quotes and escaped double quotes', () => { - const result = parser.parse( '"foo \\"bar\\""' ); - - expect( result ).toEqual( 'foo "bar"' ); - } ); - - it( 'should parse a string literal with double quotes and escaped backslashes', () => { - // eslint-disable-next-line prettier/prettier - const result = parser.parse( '"foo \\\\\\"bar\\\\\\""' ); - - expect( result ).toEqual( 'foo \\"bar\\"' ); - } ); - - it( 'should parse a string literal with single quotes', () => { - const result = parser.parse( "'foo'" ); - - expect( result ).toEqual( 'foo' ); - } ); - - it( 'should parse a string literal with single quotes and double quotes', () => { - // eslint-disable-next-line prettier/prettier - const result = parser.parse( "'foo \"bar\"'" ); - - expect( result ).toEqual( 'foo "bar"' ); - } ); - - it( 'should parse a string literal with single quotes and escaped single quotes', () => { - const result = parser.parse( "'foo \\'bar\\''" ); - - expect( result ).toEqual( "foo 'bar'" ); - } ); - - it( 'should parse a string literal with single quotes and escaped backslashes', () => { - // eslint-disable-next-line prettier/prettier - const result = parser.parse( "'foo \\\\\\'bar\\\\\\''" ); - - expect( result ).toEqual( "foo \\'bar\\'" ); - } ); - - it( 'should parse a top-level context property', () => { - const result = parser.parse( 'foo', { - context: { - foo: 'bar', - }, - } ); - - expect( result ).toEqual( 'bar' ); - } ); - - it( 'should parse a nested context property', () => { - const result = parser.parse( 'foo.bar', { - context: { - foo: { - bar: 'baz', - }, - }, - } ); - - expect( result ).toEqual( 'baz' ); - } ); - - it( 'should parse an strict equality expression', () => { - const result = parser.parse( 'foo === "bar"', { - context: { - foo: 'bar', - }, - } ); - - expect( result ).toEqual( true ); - } ); - - it( 'should parse an strict inequality expression', () => { - const result = parser.parse( 'foo !== "bar"', { - context: { - foo: 'bar', - }, - } ); - - expect( result ).toEqual( false ); - } ); - - it( 'should parse an equality expression', () => { - const result = parser.parse( 'foo == "bar"', { - context: { - foo: 'bar', - }, - } ); - - expect( result ).toEqual( true ); - } ); - - it( 'should parse an inequality expression', () => { - const result = parser.parse( 'foo != "bar"', { - context: { - foo: 'bar', - }, - } ); - - expect( result ).toEqual( false ); - } ); - - it( 'should parse a logical OR expression', () => { - const result = parser.parse( 'foo || bar', { - context: { - foo: true, - bar: false, - }, - } ); - - expect( result ).toEqual( true ); - } ); - - it( 'should parse a logical AND expression', () => { - const result = parser.parse( 'foo && bar', { - context: { - foo: true, - bar: false, - }, - } ); - - expect( result ).toEqual( false ); - } ); - - it( 'should parse a NOT expression', () => { - const result = parser.parse( '!foo', { - context: { - foo: true, - }, - } ); - - expect( result ).toEqual( false ); - } ); - - it( 'should parse a double NOT expression', () => { - const result = parser.parse( '!!foo', { - context: { - foo: true, - }, - } ); - - expect( result ).toEqual( true ); - } ); - - it( 'should parse a multiline expression', () => { - const result = parser.parse( - `foo - || bar - || baz`, - { - context: { - foo: false, - bar: false, - baz: true, - }, - } - ); - - expect( result ).toEqual( true ); - } ); - - it( 'should throw an error if the expression is invalid', () => { - expect( () => parser.parse( '= 1' ) ).toThrow(); - } ); -} ); From 18992e7891801d0b75a2c7814f6f0cb36e57ba9c Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 09:33:21 -0400 Subject: [PATCH 13/24] Implement relational expressions --- .../js/expression-evaluation/src/parser.ts | 22 ++++++++++++- .../src/test/index.test.ts | 32 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index ac71da9beb9..9e4a2d047a6 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -46,6 +46,18 @@ const grammar = ` case '!=': return leftOperand != rightOperand; break; + case '<=': + return leftOperand <= rightOperand; + break; + case '<': + return leftOperand < rightOperand; + break; + case '>=': + return leftOperand >= rightOperand; + break; + case '>': + return leftOperand > rightOperand; + break; default: return undefined; break; @@ -294,7 +306,15 @@ UnaryOperator / "+" RelationalExpression - = UnaryExpression + = head:UnaryExpression tail:(__ RelationalOperator __ UnaryExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +RelationalOperator + = "<=" + / "<" + / ">=" + / ">" EqualityExpression = head:RelationalExpression tail:(__ EqualityOperator __ RelationalExpression)* { diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index 066ffa76c4c..48efc4109b9 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -134,6 +134,38 @@ describe( 'evaluate', () => { expect( result ).toEqual( 'baz' ); } ); + it( 'should evaluate a less than or equal expression', () => { + const result = evaluate( 'foo <= 1', { + foo: 1, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a less than expression', () => { + const result = evaluate( 'foo < 1', { + foo: 1, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a greater than or equal expression', () => { + const result = evaluate( 'foo >= 1', { + foo: 1, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a greater than expression', () => { + const result = evaluate( 'foo > 1', { + foo: 1, + } ); + + expect( result ).toEqual( false ); + } ); + it( 'should evaluate an strict equality expression', () => { const result = evaluate( 'foo === "bar"', { foo: 'bar', From 99b06b67a20cde31d2f8dfd699730a1477a0c70d Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 09:38:52 -0400 Subject: [PATCH 14/24] Tests for complex expressions --- .../src/test/index.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index 48efc4109b9..749ab280ef9 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -247,6 +247,29 @@ describe( 'evaluate', () => { expect( result ).toEqual( true ); } ); + it( 'should evaluate a complex expression', () => { + const result = evaluate( + `foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" )`, + { + foo: { + bar: true, + baz: 'quux', + }, + } + ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate an expression with too many parentheses', () => { + const result = evaluate( '(((foo)))', { + foo: true, + } ); + + expect( result ).toEqual( true ); + } ); + it( 'should throw an error if the expression is invalid', () => { expect( () => evaluate( '= 1' ) ).toThrow(); } ); From e7e9e30884252a7cd3130af68e07c89af72a2d05 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 09:52:54 -0400 Subject: [PATCH 15/24] Test NOT with parenthesis --- .../js/expression-evaluation/src/test/index.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index 749ab280ef9..30312343f34 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -232,6 +232,14 @@ describe( 'evaluate', () => { expect( result ).toEqual( true ); } ); + it( 'should evaluate a NOT expression with parentheses', () => { + const result = evaluate( '!( foo )', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + it( 'should evaluate a multiline expression', () => { const result = evaluate( `foo @@ -262,7 +270,7 @@ describe( 'evaluate', () => { expect( result ).toEqual( true ); } ); - it( 'should evaluate an expression with too many parentheses', () => { + it( 'should evaluate an expression with needless parentheses', () => { const result = evaluate( '(((foo)))', { foo: true, } ); From 11653fe3278e6f179833d3cb80c69a4335079eaf Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 09:54:15 -0400 Subject: [PATCH 16/24] Rearrange tests to match grammar order --- .../src/test/index.test.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index 30312343f34..faf1876c206 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -134,6 +134,30 @@ describe( 'evaluate', () => { expect( result ).toEqual( 'baz' ); } ); + it( 'should evaluate a NOT expression', () => { + const result = evaluate( '!foo', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + + it( 'should evaluate a double NOT expression', () => { + const result = evaluate( '!!foo', { + foo: true, + } ); + + expect( result ).toEqual( true ); + } ); + + it( 'should evaluate a NOT expression with parentheses', () => { + const result = evaluate( '!( foo )', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + it( 'should evaluate a less than or equal expression', () => { const result = evaluate( 'foo <= 1', { foo: 1, @@ -216,30 +240,6 @@ describe( 'evaluate', () => { expect( result ).toEqual( false ); } ); - it( 'should evaluate a NOT expression', () => { - const result = evaluate( '!foo', { - foo: true, - } ); - - expect( result ).toEqual( false ); - } ); - - it( 'should evaluate a double NOT expression', () => { - const result = evaluate( '!!foo', { - foo: true, - } ); - - expect( result ).toEqual( true ); - } ); - - it( 'should evaluate a NOT expression with parentheses', () => { - const result = evaluate( '!( foo )', { - foo: true, - } ); - - expect( result ).toEqual( false ); - } ); - it( 'should evaluate a multiline expression', () => { const result = evaluate( `foo From 83c3ff365a10709971c8fba8ee5845658ef2a5c7 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 09:59:39 -0400 Subject: [PATCH 17/24] Implement arithmetic expressions --- .../js/expression-evaluation/src/parser.ts | 36 +++++++++++- .../src/test/index.test.ts | 56 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 9e4a2d047a6..c0b90efbf15 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -58,6 +58,21 @@ const grammar = ` case '>': return leftOperand > rightOperand; break; + case '+': + return leftOperand + rightOperand; + break; + case '-': + return leftOperand - rightOperand; + break; + case '*': + return leftOperand * rightOperand; + break; + case '/': + return leftOperand / rightOperand; + break; + case '%': + return leftOperand % rightOperand; + break; default: return undefined; break; @@ -305,8 +320,27 @@ UnaryOperator / "-" / "+" +MultiplicativeExpression + = head:UnaryExpression tail:(__ MultiplicativeOperator __ UnaryExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +MultiplicativeOperator + = "*" + / "/" + / "%" + +AdditiveExpression + = head:MultiplicativeExpression tail:(__ AdditiveOperator __ MultiplicativeExpression)* { + return evaluateBinaryExpression( head, tail ); + } + +AdditiveOperator + = "+" + / "-" + RelationalExpression - = head:UnaryExpression tail:(__ RelationalOperator __ UnaryExpression)* { + = head:AdditiveExpression tail:(__ RelationalOperator __ AdditiveExpression)* { return evaluateBinaryExpression( head, tail ); } diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index faf1876c206..10ba1b2dae9 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -158,6 +158,62 @@ describe( 'evaluate', () => { expect( result ).toEqual( false ); } ); + it( 'should evaluate a multiplication expression', () => { + const result = evaluate( 'foo * 2', { + foo: 2, + } ); + + expect( result ).toEqual( 4 ); + } ); + + it( 'should evaluate a division expression', () => { + const result = evaluate( 'foo / 2', { + foo: 4, + } ); + + expect( result ).toEqual( 2 ); + } ); + + it( 'should evaluate a modulo expression', () => { + const result = evaluate( 'foo % 2', { + foo: 5, + } ); + + expect( result ).toEqual( 1 ); + } ); + + it( 'should evaluate an addition expression', () => { + const result = evaluate( 'foo + 2', { + foo: 3, + } ); + + expect( result ).toEqual( 5 ); + } ); + + it( 'should evaluate a subtraction expression', () => { + const result = evaluate( 'foo - 2', { + foo: 5, + } ); + + expect( result ).toEqual( 3 ); + } ); + + it( 'should evaluate a complex arithmetic expression', () => { + const result = evaluate( 'foo * 2 + 1', { + foo: 3, + } ); + + expect( result ).toEqual( 7 ); + } ); + + it( 'should evaluate a complex arithmetic expression with parenthesis', () => { + const result = evaluate( 'foo * (2 + 1)', { + foo: 3, + } ); + + expect( result ).toEqual( 9 ); + } ); + it( 'should evaluate a less than or equal expression', () => { const result = evaluate( 'foo <= 1', { foo: 1, From 1812a76f8d8fb832401097bbe589fecab120a99f Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 10:02:15 -0400 Subject: [PATCH 18/24] Test with complex expression with arithmetic, relational, and logical operators --- .../src/test/index.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index 10ba1b2dae9..236f56d54de 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -326,6 +326,23 @@ describe( 'evaluate', () => { expect( result ).toEqual( true ); } ); + it( 'should evaluate a complex expression with arithmetic, relational, and logical operators', () => { + const result = evaluate( + `foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 )`, + { + foo: { + bar: true, + baz: 'quux', + quux: 10, + }, + } + ); + + expect( result ).toEqual( false ); + } ); + it( 'should evaluate an expression with needless parentheses', () => { const result = evaluate( '(((foo)))', { foo: true, From 06460312178eb6588a6ff170359065ad9937681d Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 15:47:06 -0400 Subject: [PATCH 19/24] Implement multiline comments and be more forgiving of extra whitespace --- .../js/expression-evaluation/src/parser.ts | 16 +++- .../src/test/index.test.ts | 90 +++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index c0b90efbf15..42c38f515b6 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -104,16 +104,22 @@ LineTerminatorSequence / "\\u2028" / "\\u2029" +Comment "comment" + = MultiLineComment + +MultiLineComment + = "/*" (!"*/" SourceCharacter)* "*/" + __ "skipped" - = (WhiteSpace / LineTerminatorSequence )* + = (WhiteSpace / LineTerminatorSequence / Comment)* IdentifierPath - = variable:Identifier accessor:("." Identifier)* { + = variable:Identifier accessor:(__ "." __ Identifier)* { const path = variable.split( '.' ); let result = path.reduce( ( nextObject, propertyName ) => nextObject[ propertyName ], options.context ); for ( let i = 0; i < accessor.length; i++ ) { - result = result[ accessor[ i ][ 1 ] ]; + result = result[ accessor[ i ][ 3 ] ]; } return result; @@ -378,7 +384,9 @@ LogicalOrOperator = "||" Expression - = LogicalOrExpression + = __ expression:LogicalOrExpression __ { + return expression; + } `; export const parser = peggy.generate( grammar ); diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index 236f56d54de..c7d8e4d1891 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -116,6 +116,12 @@ describe( 'evaluate', () => { expect( result ).toEqual( "foo \\'bar\\'" ); } ); + it( 'should evaluate a literal with whitespace around it', () => { + const result = evaluate( ' 23 ' ); + + expect( result ).toEqual( 23 ); + } ); + it( 'should evaluate a top-level context property', () => { const result = evaluate( 'foo', { foo: 'bar', @@ -124,6 +130,14 @@ describe( 'evaluate', () => { expect( result ).toEqual( 'bar' ); } ); + it( 'should evaluate a top-level context property with whitespace', () => { + const result = evaluate( ' foo ', { + foo: 'bar', + } ); + + expect( result ).toEqual( 'bar' ); + } ); + it( 'should evaluate a nested context property', () => { const result = evaluate( 'foo.bar', { foo: { @@ -134,6 +148,30 @@ describe( 'evaluate', () => { expect( result ).toEqual( 'baz' ); } ); + it( 'should evaluate a nested context property with whitespace', () => { + const result = evaluate( 'foo. bar', { + foo: { + bar: 'baz', + }, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + + it( 'should evaluate a nested context property with multiple lines', () => { + const result = evaluate( + `foo. + bar`, + { + foo: { + bar: 'baz', + }, + } + ); + + expect( result ).toEqual( 'baz' ); + } ); + it( 'should evaluate a NOT expression', () => { const result = evaluate( '!foo', { foo: true, @@ -158,6 +196,14 @@ describe( 'evaluate', () => { expect( result ).toEqual( false ); } ); + it( 'should evaluate a NOT expression with parentheses and spaces', () => { + const result = evaluate( '! ( foo ) ', { + foo: true, + } ); + + expect( result ).toEqual( false ); + } ); + it( 'should evaluate a multiplication expression', () => { const result = evaluate( 'foo * 2', { foo: 2, @@ -351,6 +397,50 @@ describe( 'evaluate', () => { expect( result ).toEqual( true ); } ); + it( 'should evaluate an expression with a multiline comment at the end', () => { + const result = evaluate( 'foo /* + 23 */', { + foo: 5, + } ); + + expect( result ).toEqual( 5 ); + } ); + + it( 'should evaluate an expression with a multiline comment at the beginning', () => { + const result = evaluate( '/* 23 + */ foo', { + foo: 5, + } ); + + expect( result ).toEqual( 5 ); + } ); + + it( 'should evaluate an expression with a multiline comment in the middle', () => { + const result = evaluate( 'foo + /* 23 */ bar', { + foo: 5, + bar: 3, + } ); + + expect( result ).toEqual( 8 ); + } ); + + it( 'should evaluate a multiline expression with a multiline comment', () => { + const result = evaluate( + `foo + /* + + bar + + boo + */ + + baz`, + { + foo: 5, + bar: 23, + boo: 6, + baz: 3, + } + ); + + expect( result ).toEqual( 8 ); + } ); + it( 'should throw an error if the expression is invalid', () => { expect( () => evaluate( '= 1' ) ).toThrow(); } ); From eb71f43bdc052d787d96041673506a8d1db66d2b Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 19:52:39 -0400 Subject: [PATCH 20/24] Implement conditional expressions --- .../js/expression-evaluation/src/parser.ts | 8 ++++- .../src/test/index.test.ts | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 42c38f515b6..4487619bb83 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -383,8 +383,14 @@ LogicalOrExpression LogicalOrOperator = "||" +ConditionalExpression + = test:LogicalOrExpression __ "?" __ consequent:ConditionalExpression __ ":" __ alternate:ConditionalExpression { + return test ? consequent : alternate; + } + / LogicalOrExpression + Expression - = __ expression:LogicalOrExpression __ { + = __ expression:ConditionalExpression __ { return expression; } `; diff --git a/packages/js/expression-evaluation/src/test/index.test.ts b/packages/js/expression-evaluation/src/test/index.test.ts index c7d8e4d1891..7b5d3dc4284 100644 --- a/packages/js/expression-evaluation/src/test/index.test.ts +++ b/packages/js/expression-evaluation/src/test/index.test.ts @@ -324,6 +324,22 @@ describe( 'evaluate', () => { expect( result ).toEqual( false ); } ); + it( 'should evaluate a conditional expression that is true', () => { + const result = evaluate( 'foo ? "bar" : "baz"', { + foo: true, + } ); + + expect( result ).toEqual( 'bar' ); + } ); + + it( 'should evaluate a conditional expression that is false', () => { + const result = evaluate( 'foo ? "bar" : "baz"', { + foo: false, + } ); + + expect( result ).toEqual( 'baz' ); + } ); + it( 'should evaluate a logical OR expression', () => { const result = evaluate( 'foo || bar', { foo: true, @@ -389,6 +405,25 @@ describe( 'evaluate', () => { expect( result ).toEqual( false ); } ); + it( 'should evaluate a complex expression with conditional, arithmetic, relational, and logical operators', () => { + const result = evaluate( + `foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 ) + ? "boo" + : "baa"`, + { + foo: { + bar: true, + baz: 'quux', + quux: 10, + }, + } + ); + + expect( result ).toEqual( 'baa' ); + } ); + it( 'should evaluate an expression with needless parentheses', () => { const result = evaluate( '(((foo)))', { foo: true, From fd2c07cac0329bbfd0d2f2b24d97c59c11ae6db2 Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 20:12:40 -0400 Subject: [PATCH 21/24] Clean up ConditionalExpression rule --- packages/js/expression-evaluation/src/parser.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/js/expression-evaluation/src/parser.ts b/packages/js/expression-evaluation/src/parser.ts index 4487619bb83..2fdb9470f0c 100644 --- a/packages/js/expression-evaluation/src/parser.ts +++ b/packages/js/expression-evaluation/src/parser.ts @@ -384,11 +384,17 @@ LogicalOrOperator = "||" ConditionalExpression - = test:LogicalOrExpression __ "?" __ consequent:ConditionalExpression __ ":" __ alternate:ConditionalExpression { - return test ? consequent : alternate; + = condition:LogicalOrExpression __ ConditionalTrueOperator __ expressionIfTrue:ConditionalExpression __ ConditionalFalseOperator __ expressionIfFalse:ConditionalExpression { + return condition ? expressionIfTrue : expressionIfFalse; } / LogicalOrExpression +ConditionalTrueOperator + = "?" + +ConditionalFalseOperator + = ":" + Expression = __ expression:ConditionalExpression __ { return expression; From 18b81b32faf9c33dc39d8a615fe395a9e962c52a Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Wed, 4 Oct 2023 22:28:47 -0400 Subject: [PATCH 22/24] Update lock file --- pnpm-lock.yaml | 318 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 264 insertions(+), 54 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8ce8dfa8dd..91c47df0054 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1825,6 +1825,49 @@ importers: specifier: ^5.1.6 version: 5.1.6 + packages/js/expression-evaluation: + dependencies: + '@wordpress/i18n': + specifier: wp-6.0 + version: 4.6.1 + peggy: + specifier: ^3.0.2 + version: 3.0.2 + devDependencies: + '@babel/core': + specifier: ^7.17.5 + version: 7.21.3 + '@types/jest': + specifier: ^27.4.1 + version: 27.4.1 + '@woocommerce/eslint-plugin': + specifier: workspace:* + version: link:../eslint-plugin + '@woocommerce/internal-js-tests': + specifier: workspace:* + version: link:../internal-js-tests + concurrently: + specifier: ^7.0.0 + version: 7.0.0 + eslint: + specifier: ^8.32.0 + version: 8.32.0 + jest: + specifier: ^27.5.1 + version: 27.5.1 + jest-cli: + specifier: ^27.5.1 + version: 27.5.1 + rimraf: + specifier: ^3.0.2 + version: 3.0.2 + ts-jest: + specifier: ^27.1.3 + version: 27.1.3(@babel/core@7.21.3)(@types/jest@27.4.1)(jest@27.5.1)(typescript@5.1.6) + typescript: + specifier: ^5.1.6 + version: 5.1.6 + packages/js/extend-cart-checkout-block: {} packages/js/internal-e2e-builds: @@ -4391,7 +4434,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@jridgewell/trace-mapping': 0.3.19 + '@jridgewell/trace-mapping': 0.3.16 commander: 4.1.1 convert-source-map: 1.8.0 fs-readdir-recursive: 1.1.0 @@ -4635,6 +4678,32 @@ packages: browserslist: 4.19.3 semver: 6.3.0 + /@babel/helper-compilation-targets@7.17.7(@babel/core@7.12.9): + resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.21.0 + '@babel/core': 7.12.9 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.19.3 + semver: 6.3.0 + dev: true + + /@babel/helper-compilation-targets@7.17.7(@babel/core@7.17.8): + resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.21.0 + '@babel/core': 7.17.8 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.19.3 + semver: 6.3.0 + dev: true + /@babel/helper-compilation-targets@7.17.7(@babel/core@7.21.3): resolution: {integrity: sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==} engines: {node: '>=6.9.0'} @@ -4920,7 +4989,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.17.8) - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.21.5 debug: 4.3.4(supports-color@9.2.2) lodash.debounce: 4.0.8 resolve: 1.22.1 @@ -4936,7 +5005,7 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.21.5 debug: 4.3.4(supports-color@9.2.2) lodash.debounce: 4.0.8 resolve: 1.22.1 @@ -5581,7 +5650,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.12.9) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.12.9) transitivePeerDependencies: @@ -5595,7 +5664,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.17.8) '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.17.8) transitivePeerDependencies: @@ -6323,7 +6392,7 @@ packages: '@babel/compat-data': 7.21.0 '@babel/core': 7.12.9 '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.12.9) - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.12.9) '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.12.9) dev: true @@ -6337,7 +6406,7 @@ packages: '@babel/compat-data': 7.21.0 '@babel/core': 7.17.8 '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.17.8) - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.17.8) '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.17.8) dev: true @@ -6488,7 +6557,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.12.9) dev: true @@ -6500,7 +6569,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.17.8) dev: true @@ -6724,7 +6793,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.12.9) - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 dev: true /@babel/plugin-proposal-unicode-property-regex@7.16.7(@babel/core@7.17.8): @@ -6735,7 +6804,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.17.8) - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 dev: true /@babel/plugin-proposal-unicode-property-regex@7.16.7(@babel/core@7.21.3): @@ -7505,8 +7574,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-module-imports': 7.16.7 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-remap-async-to-generator': 7.16.8 transitivePeerDependencies: - supports-color @@ -7533,8 +7602,8 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-module-imports': 7.16.7 + '@babel/helper-plugin-utils': 7.21.5 '@babel/helper-remap-async-to-generator': 7.16.8 transitivePeerDependencies: - supports-color @@ -8179,7 +8248,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 dev: true /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.21.3): @@ -8190,7 +8259,7 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 /@babel/plugin-transform-flow-strip-types@7.16.7(@babel/core@7.12.9): resolution: {integrity: sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg==} @@ -8608,12 +8677,10 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-module-transforms': 7.21.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.18.6 + '@babel/helper-module-transforms': 7.22.15(@babel/core@7.12.9) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color dev: true /@babel/plugin-transform-modules-commonjs@7.17.7(@babel/core@7.17.8): @@ -8623,12 +8690,10 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-module-transforms': 7.21.2 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.18.6 + '@babel/helper-module-transforms': 7.22.15(@babel/core@7.17.8) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color dev: true /@babel/plugin-transform-modules-commonjs@7.17.7(@babel/core@7.21.3): @@ -8665,8 +8730,8 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-module-transforms': 7.22.15(@babel/core@7.17.8) - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-simple-access': 7.20.2 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 dev: true /@babel/plugin-transform-modules-commonjs@7.21.2(@babel/core@7.21.3): @@ -8677,8 +8742,8 @@ packages: dependencies: '@babel/core': 7.21.3 '@babel/helper-module-transforms': 7.22.15(@babel/core@7.21.3) - '@babel/helper-plugin-utils': 7.21.5 - '@babel/helper-simple-access': 7.20.2 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 /@babel/plugin-transform-modules-commonjs@7.22.15(@babel/core@7.12.9): resolution: {integrity: sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==} @@ -9395,7 +9460,7 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.22.15 + '@babel/helper-module-imports': 7.21.4 '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.12.9) '@babel/types': 7.22.15 @@ -9408,7 +9473,7 @@ packages: dependencies: '@babel/core': 7.17.8 '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.22.15 + '@babel/helper-module-imports': 7.21.4 '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.17.8) '@babel/types': 7.22.15 @@ -10290,7 +10355,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.12.9 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.12.9) + '@babel/helper-compilation-targets': 7.17.7(@babel/core@7.12.9) '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-option': 7.16.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7(@babel/core@7.12.9) @@ -10357,12 +10422,12 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.16.7(@babel/core@7.12.9) '@babel/plugin-transform-unicode-regex': 7.16.7(@babel/core@7.12.9) '@babel/preset-modules': 0.1.5(@babel/core@7.12.9) - '@babel/types': 7.22.15 + '@babel/types': 7.17.0 babel-plugin-polyfill-corejs2: 0.3.0(@babel/core@7.12.9) babel-plugin-polyfill-corejs3: 0.5.2(@babel/core@7.12.9) babel-plugin-polyfill-regenerator: 0.3.0(@babel/core@7.12.9) core-js-compat: 3.21.1 - semver: 6.3.1 + semver: 6.3.0 transitivePeerDependencies: - supports-color dev: true @@ -10375,7 +10440,7 @@ packages: dependencies: '@babel/compat-data': 7.17.7 '@babel/core': 7.17.8 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.17.8) + '@babel/helper-compilation-targets': 7.17.7(@babel/core@7.17.8) '@babel/helper-plugin-utils': 7.18.9 '@babel/helper-validator-option': 7.16.7 '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.16.7(@babel/core@7.17.8) @@ -10442,12 +10507,12 @@ packages: '@babel/plugin-transform-unicode-escapes': 7.16.7(@babel/core@7.17.8) '@babel/plugin-transform-unicode-regex': 7.16.7(@babel/core@7.17.8) '@babel/preset-modules': 0.1.5(@babel/core@7.17.8) - '@babel/types': 7.22.15 + '@babel/types': 7.17.0 babel-plugin-polyfill-corejs2: 0.3.0(@babel/core@7.17.8) babel-plugin-polyfill-corejs3: 0.5.2(@babel/core@7.17.8) babel-plugin-polyfill-regenerator: 0.3.0(@babel/core@7.17.8) core-js-compat: 3.21.1 - semver: 6.3.1 + semver: 6.3.0 transitivePeerDependencies: - supports-color dev: true @@ -12065,6 +12130,49 @@ packages: - ts-node dev: true + /@jest/core@29.6.2: + resolution: {integrity: sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.6.2 + '@jest/reporters': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/transform': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.2.0 + exit: 0.1.2 + graceful-fs: 4.2.9 + jest-changed-files: 29.5.0 + jest-config: 29.6.2(@types/node@16.18.21) + jest-haste-map: 29.6.2 + jest-message-util: 29.6.2 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-resolve-dependencies: 29.6.2 + jest-runner: 29.6.2 + jest-runtime: 29.6.2 + jest-snapshot: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + jest-watcher: 29.6.2 + micromatch: 4.0.5 + pretty-format: 29.6.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: false + /@jest/core@29.6.2(ts-node@10.9.1): resolution: {integrity: sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12106,6 +12214,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /@jest/create-cache-key-function@27.5.1: resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} @@ -12940,7 +13049,6 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 - dev: true /@jridgewell/trace-mapping@0.3.17: resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} @@ -21308,7 +21416,7 @@ packages: '@babel/runtime': 7.21.0 '@wordpress/keycodes': 2.19.3 '@wordpress/url': 2.22.2(react-native@0.70.0) - jest: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + jest: 29.6.2(@types/node@16.18.21) lodash: 4.17.21 node-fetch: 2.6.7 puppeteer: 2.1.1 @@ -21329,7 +21437,7 @@ packages: '@wordpress/keycodes': 3.6.1 '@wordpress/url': 3.7.1 form-data: 4.0.0 - jest: 29.6.2(@types/node@16.18.21)(ts-node@10.9.1) + jest: 29.6.2(@types/node@16.18.21) lodash: 4.17.21 node-fetch: 2.6.7 puppeteer-core: 19.7.3(typescript@5.1.6) @@ -24777,7 +24885,7 @@ packages: '@babel/compat-data': 7.21.0 '@babel/core': 7.17.8 '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.17.8) - semver: 6.3.1 + semver: 6.3.0 transitivePeerDependencies: - supports-color dev: true @@ -24790,7 +24898,7 @@ packages: '@babel/compat-data': 7.21.0 '@babel/core': 7.21.3 '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.3) - semver: 6.3.1 + semver: 6.3.0 transitivePeerDependencies: - supports-color dev: true @@ -28076,12 +28184,8 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - /decimal.js@10.3.1: - resolution: {integrity: sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==} - /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true /decode-uri-component@0.2.0: resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} @@ -28140,7 +28244,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /deepsignal@1.3.6(@preact/signals@1.2.1)(preact@10.17.1): resolution: {integrity: sha512-yjd+vtiznL6YaMptOsKnEKkPr60OEApa+LRe+Qe6Ile/RfCOrELKk/YM3qVpXFZiyOI3Ng67GDEyjAlqVc697g==} @@ -34313,6 +34416,35 @@ packages: - ts-node dev: true + /jest-cli@29.6.2(@types/node@16.18.21): + resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.2 + '@jest/test-result': 29.6.2 + '@jest/types': 29.6.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.9 + import-local: 3.0.3 + jest-config: 29.6.2(@types/node@16.18.21) + jest-util: 29.6.2 + jest-validate: 29.6.2 + prompts: 2.4.2 + yargs: 17.5.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: false + /jest-cli@29.6.2(@types/node@16.18.21)(ts-node@10.9.1): resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -34340,6 +34472,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /jest-config@24.9.0: resolution: {integrity: sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==} @@ -34447,7 +34580,7 @@ packages: babel-jest: 27.5.1(@babel/core@7.21.3) chalk: 4.1.2 ci-info: 3.2.0 - deepmerge: 4.3.0 + deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.9 jest-circus: 27.5.1 @@ -34511,6 +34644,46 @@ packages: - supports-color dev: true + /jest-config@29.6.2(@types/node@16.18.21): + resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@jest/test-sequencer': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 16.18.21 + babel-jest: 29.6.2(@babel/core@7.21.3) + chalk: 4.1.2 + ci-info: 3.2.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.9 + jest-circus: 29.6.2 + jest-environment-node: 29.6.2 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.6.2 + jest-runner: 29.6.2 + jest-util: 29.6.2 + jest-validate: 29.6.2 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.6.2 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: false + /jest-config@29.6.2(@types/node@16.18.21)(ts-node@10.9.1): resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -34550,6 +34723,7 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color + dev: true /jest-dev-server@4.4.0: resolution: {integrity: sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A==} @@ -36580,6 +36754,27 @@ packages: - ts-node dev: true + /jest@29.6.2(@types/node@16.18.21): + resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.6.2 + '@jest/types': 29.6.1 + import-local: 3.0.3 + jest-cli: 29.6.2(@types/node@16.18.21) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: false + /jest@29.6.2(@types/node@16.18.21)(ts-node@10.9.1): resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -36599,6 +36794,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /jmespath@0.16.0: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} @@ -36851,7 +37047,7 @@ packages: cssom: 0.4.4 cssstyle: 2.3.0 data-urls: 2.0.0 - decimal.js: 10.3.1 + decimal.js: 10.4.3 domexception: 2.0.1 escodegen: 2.0.0 form-data: 3.0.1 @@ -40461,6 +40657,15 @@ packages: sha.js: 2.4.11 dev: true + /peggy@3.0.2: + resolution: {integrity: sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==} + engines: {node: '>=14'} + hasBin: true + dependencies: + commander: 10.0.1 + source-map-generator: 0.8.0 + dev: false + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -42349,7 +42554,7 @@ packages: is-touch-device: 1.0.1 lodash: 4.17.21 moment: 2.29.4 - object.assign: 4.1.4 + object.assign: 4.1.2 object.values: 1.1.5 prop-types: 15.8.1 raf: 3.4.1 @@ -44841,6 +45046,11 @@ packages: /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + /source-map-generator@0.8.0: + resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==} + engines: {node: '>= 10'} + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -46634,7 +46844,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.5.0 + semver: 7.5.3 typescript: 5.1.6 yargs-parser: 20.2.9 dev: true @@ -46669,7 +46879,7 @@ packages: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.5.0 + semver: 7.5.3 typescript: 5.1.6 yargs-parser: 20.2.9 dev: true From 7a950f51b68f610759219aaf60d367d01bf12f8b Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Sun, 8 Oct 2023 07:03:17 -0400 Subject: [PATCH 23/24] Changelog --- .../changelog/add-expression-evaluation-package | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/js/expression-evaluation/changelog/add-expression-evaluation-package diff --git a/packages/js/expression-evaluation/changelog/add-expression-evaluation-package b/packages/js/expression-evaluation/changelog/add-expression-evaluation-package new file mode 100644 index 00000000000..3ad65acf293 --- /dev/null +++ b/packages/js/expression-evaluation/changelog/add-expression-evaluation-package @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Initial @woocommerce/expression-evaluation package. From 2abd3eee3840761c15f029fa9b1790efc59e146d Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Sun, 8 Oct 2023 08:13:54 -0400 Subject: [PATCH 24/24] Initial documentation --- packages/js/expression-evaluation/README.md | 313 ++++++++++++++++++++ 1 file changed, 313 insertions(+) diff --git a/packages/js/expression-evaluation/README.md b/packages/js/expression-evaluation/README.md index 0fff47727d1..583c87b9b2b 100644 --- a/packages/js/expression-evaluation/README.md +++ b/packages/js/expression-evaluation/README.md @@ -1 +1,314 @@ # @woocommerce/expression-evaluation + +Evaluation of JavaScript-like expressions in an optional context. + +Examples of simple expressions: + +```js +1 + 2 +``` + +```js +foo === 'bar' +``` + +```js +foo ? 'bar' : 'baz' +``` + +Examples of complex expressions: + +```js +foo.bar.baz === 'qux' +``` + +```js +foo.bar + && ( foo.bar.baz === 'qux' || foo.baz === 'quux' ) +``` + +```js +foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 ) +``` + +```js +foo.bar + && ( foo.baz === "qux" || foo.baz === "quux" ) + && ( foo.quux > 1 && foo.quux <= 5 ) + ? "boo" + : "baa" +``` + +```js +foo + + 5 + /* This is a comment */ + * ( bar ? baz : qux ) +``` + +## API + +### evaluate + +Evaluates an expression in an optional context. + +#### Usage + +```js +import { evaluate } from '@woocommerce/expression-evaluation'; + +const result = evaluate( '1 + foo', { foo: 2 } ); + +console.log( result ); // 3 +``` + +#### Parameters + +- _expression_ `string`: The expression to evaluate. +- _context_ `Object`: Optional. The context to evaluate the expression in. Variables in the expression will be looked up in this object. + +#### Returns + +- `any`: The result of the expression evaluation. + +## Expression syntax + +### Grammar and types + +The expression syntax is based on JavaScript. The formal grammar is defined in [parser.ts](./src/parser.ts). + +An expression consists of a single statement. + +Features like `if` statements, `for` loops, function calls, and variable assignments, are not supported. + +The following types are supported: + +- `null` +- Boolean: `true` and `false` +- Number: An integer or floating point number. +- String: A sequence of characters that represent text. + +### Literals + +Values in an expression can be written as literals. + +#### null + +```js +null +``` + +#### Boolean + +```js +true +false +``` + +#### Number + +```js +1 +5.23 +-9 +``` + +#### String + +String literals can be written with single or double quotes. This can be helpful if the string contains a single or double quote. + +```js +'foo' +"foo" +'foo "bar"' +"foo 'bar'" +``` + +Quotes can be escaped with a backslash. + +```js +'foo \'bar\'' +"foo \"bar\"" +``` + +### Context variables + +Variables can be used in an expression. The value of a variable is looked up in the context. + +```js +const result = evaluate( 'foo', { foo: 1 } ); + +console.log( result ); // 1 +``` + +Nested properties can be accessed with the dot operator. + +```js +const result = evaluate( 'foo.bar', { foo: { bar: 1 } } ); + +console.log( result ); // 1 +``` + +### Operators + +The following operators are supported. + +#### Comparison operators + +##### Equal (`==`) + +Returns `true` if the operands are equal. + +```js +1 == 1 +``` + +##### Not equal (`!=`) + +Returns `true` if the operands are not equal. + +```js +1 != 2 +``` + +##### Strict equal (`===`) + +Returns `true` if the operands are equal and of the same type. + +```js +1 === 1 +``` + +##### Strict not equal (`!==`) + +Returns `true` if the operands are not equal and/or not of the same type. + +```js +1 !== "1" +``` + +##### Greater than (`>`) + +Returns `true` if the left operand is greater than the right operand. + +```js +2 > 1 +``` + +##### Greater than or equal (`>=`) + +Returns `true` if the left operand is greater than or equal to the right operand. + +```js +2 >= 2 +``` + +##### Less than (`<`) + +Returns `true` if the left operand is less than the right operand. + +```js +1 < 2 +``` + +##### Less than or equal (`<=`) + +Returns `true` if the left operand is less than or equal to the right operand. + +```js +2 <= 2 +``` + +#### Arithmetic operators + +##### Addition (`+`) + +Returns the sum of two operands. + +```js +1 + 2 +``` + +##### Subtraction (`-`) + +Returns the difference of two operands. + +```js +2 - 1 +``` + +##### Multiplication (`*`) + +Returns the product of two operands. + +```js +2 * 3 +``` + +##### Division (`/`) + +Returns the quotient of two operands. + +```js +6 / 2 +``` + +##### Modulus (`%`) + +Returns the remainder of two operands. + +```js +5 % 2 +``` + +##### Negation (`-`) + +Returns the negation of an operand. + +```js +-1 +``` + +#### Logical operators + +##### Logical AND (`&&`) + +Returns `true` if both operands are `true`. + +```js +true && true +``` + +##### Logical OR (`||`) + +Returns `true` if either operand is `true`. + +```js +true || false +``` + +##### Logical NOT (`!`) + +Returns `true` if the operand is `false`. + +```js +!false +``` + +#### Conditional (ternary) operator + +Returns the first value if the condition is `true`, otherwise it returns the second value. + +```js +true ? 1 : 2 +``` + +### Comments + +Comments can be used to document an expression. Comments are treated as whitespace and are ignored by the parser. + +```js +/* This is a comment */ +```