From ffa065cb498d0c3c36392bea403066cb4abba3f1 Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Fri, 23 Jan 2026 20:37:07 +0100 Subject: [PATCH 1/5] Improve documentation --- CONTRIBUTING.md | 249 +++++++++++++ README.md | 40 ++- docs/advanced-features.md | 266 ++++++++++++++ docs/enhancements.md | 244 ++----------- docs/expression.md | 66 ++-- docs/language-service.md | 2 + docs/migration.md | 167 +++++++++ docs/parser.md | 253 +++++++++++-- docs/performance.md | 2 + docs/quick-reference.md | 163 +++++++++ docs/syntax.md | 434 +++++++++++++---------- src/language-service/diagnostics.ts | 2 +- src/language-service/language-service.ts | 2 +- src/language-service/ls-utils.ts | 126 ++++++- 14 files changed, 1533 insertions(+), 483 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/advanced-features.md create mode 100644 docs/migration.md create mode 100644 docs/quick-reference.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c0b1920a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,249 @@ +# Contributing to expr-eval + +Thank you for your interest in contributing to expr-eval! This document provides guidelines and information for contributors. + +## Getting Started + +### Prerequisites + +- Node.js 18 or higher +- npm 9 or higher + +### Setup + +```bash +# Clone the repository +git clone https://github.com/user/expr-eval.git +cd expr-eval + +# Install dependencies +npm install + +# Run tests to verify setup +npm test +``` + +## Development Workflow + +### Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage +``` + +### Running Benchmarks + +```bash +# Run all benchmarks +npm run bench + +# Run specific benchmark categories +npm run bench:parsing +npm run bench:evaluation +npm run bench:memory +``` + +See [Performance Testing Guide](docs/performance.md) for details on interpreting benchmark results. + +### Building + +```bash +# Build the library +npm run build + +# Build and watch for changes +npm run build:watch +``` + +### Linting + +```bash +# Run ESLint +npm run lint + +# Fix auto-fixable issues +npm run lint:fix +``` + +## Project Structure + +``` +expr-eval/ +├── src/ # Source code +│ ├── index.ts # Main entry point +│ ├── config/ # Parser configuration +│ ├── core/ # Core expression operations +│ ├── errors/ # Error types and handling +│ ├── functions/ # Built-in functions +│ │ ├── array/ # Array functions +│ │ ├── math/ # Math functions +│ │ ├── object/ # Object functions +│ │ ├── string/ # String functions +│ │ └── utility/ # Utility functions +│ ├── language-service/ # IDE integration (LSP) +│ ├── operators/ # Operator implementations +│ │ ├── binary/ # Binary operators +│ │ └── unary/ # Unary operators +│ ├── parsing/ # Parser and tokenizer +│ ├── types/ # TypeScript type definitions +│ └── validation/ # Expression validation +├── test/ # Test files (mirrors src structure) +├── benchmarks/ # Performance benchmarks +├── docs/ # Documentation +└── samples/ # Example integrations +``` + +## Making Changes + +### Code Style + +- Use TypeScript for all new code +- Follow the existing code style (enforced by ESLint) +- Use meaningful variable and function names +- Add JSDoc/TSDoc comments for public APIs + +### Adding a New Function + +1. **Create the function** in the appropriate directory under `src/functions/` +2. **Export it** from the directory's `index.ts` +3. **Register it** in `src/functions/index.ts` +4. **Add tests** in the corresponding `test/functions/` directory +5. **Document it** in `docs/syntax.md` under the appropriate section +6. **Update quick-reference.md** if it's a commonly-used function + +Example structure for a new string function: + +```typescript +// src/functions/string/my-function.ts +import { Value } from '../../types'; + +/** + * Description of what the function does. + * @param str - The input string + * @returns The transformed string + */ +export function myFunction(str: Value): string | undefined { + if (typeof str !== 'string') return undefined; + // Implementation + return str.toUpperCase(); +} +``` + +### Adding a New Operator + +1. Create the operator in `src/operators/binary/` or `src/operators/unary/` +2. Register it in the parser configuration +3. Add tests +4. Document in `docs/syntax.md` + +### Modifying the Parser + +Changes to the parser (`src/parsing/`) require extra care: + +1. Ensure backward compatibility +2. Add comprehensive tests for edge cases +3. Run benchmarks to check performance impact +4. Update documentation if syntax changes + +## Testing Guidelines + +### Writing Tests + +- Use descriptive test names that explain what's being tested +- Test both success cases and error cases +- Test edge cases (empty arrays, undefined values, etc.) +- Group related tests using `describe` blocks + +```typescript +describe('myFunction', () => { + it('should transform a simple string', () => { + expect(Parser.evaluate('myFunction("hello")')).toBe('HELLO'); + }); + + it('should return undefined for non-string input', () => { + expect(Parser.evaluate('myFunction(123)')).toBeUndefined(); + }); + + it('should handle empty strings', () => { + expect(Parser.evaluate('myFunction("")')).toBe(''); + }); +}); +``` + +### Test Coverage + +Aim for high test coverage, especially for: +- All public API functions +- Error handling paths +- Edge cases and boundary conditions + +## Documentation + +### Updating Documentation + +When making changes, update the relevant documentation: + +| Change Type | Documents to Update | +|-------------|---------------------| +| New function | `docs/syntax.md`, `docs/quick-reference.md` | +| New operator | `docs/syntax.md`, `docs/quick-reference.md` | +| Parser options | `docs/parser.md` | +| Expression methods | `docs/expression.md` | +| Language service | `docs/language-service.md` | +| Breaking changes | `BREAKING_CHANGES.md` | + +### Documentation Audiences + +Remember that documentation serves different audiences: + +- **Expression writers**: Non-programmers writing expressions (focus on `syntax.md`, `quick-reference.md`) +- **Developers**: Programmers integrating the library (focus on `parser.md`, `expression.md`) +- **Contributors**: People working on the library itself (focus on `performance.md`, this file) + +## Pull Request Process + +1. **Fork** the repository and create a feature branch +2. **Make your changes** following the guidelines above +3. **Add tests** for any new functionality +4. **Run the test suite** and ensure all tests pass +5. **Run benchmarks** if your change might affect performance +6. **Update documentation** as needed +7. **Submit a pull request** with a clear description of changes + +### PR Checklist + +- [ ] Tests pass (`npm test`) +- [ ] Linting passes (`npm run lint`) +- [ ] Documentation updated (if applicable) +- [ ] BREAKING_CHANGES.md updated (if applicable) +- [ ] Benchmarks run (if performance-sensitive) + +## Reporting Issues + +When reporting issues, please include: + +- A clear description of the problem +- Steps to reproduce +- Expected vs actual behavior +- Version of expr-eval +- Node.js version +- Minimal code example demonstrating the issue + +## Security + +If you discover a security vulnerability, please do NOT open a public issue. Instead, report it privately following the security policy in the repository. + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project (see [LICENSE.txt](LICENSE.txt)). + +## Questions? + +If you have questions about contributing, feel free to open a discussion or issue on GitHub. diff --git a/README.md b/README.md index 988e63ee..a42e0053 100644 --- a/README.md +++ b/README.md @@ -20,26 +20,46 @@ npm install @pro-fa/expr-eval ## Quick Start ```js -const Parser = require('@pro-fa/expr-eval').Parser; +import { Parser } from '@pro-fa/expr-eval'; const parser = new Parser(); -let expr = parser.parse('2 * x + 1'); +const expr = parser.parse('2 * x + 1'); console.log(expr.evaluate({ x: 3 })); // 7 -// or -Parser.evaluate('6 * x', { x: 7 }) // 42 +// or evaluate directly +Parser.evaluate('6 * x', { x: 7 }); // 42 ``` ## Documentation +### For Expression Writers + +If you're writing expressions in an application powered by expr-eval: + +| Document | Description | +|:---------|:------------| +| [Quick Reference](docs/quick-reference.md) | Cheat sheet of operators, functions, and syntax | +| [Expression Syntax](docs/syntax.md) | Complete syntax reference with examples | + +### For Developers + +If you're integrating expr-eval into your project: + +| Document | Description | +|:---------|:------------| +| [Parser](docs/parser.md) | Parser configuration, methods, and customization | +| [Expression](docs/expression.md) | Expression object methods: evaluate, simplify, variables, toJSFunction | +| [Advanced Features](docs/advanced-features.md) | Promises, custom resolution, type conversion, operator customization | +| [Language Service](docs/language-service.md) | IDE integration: completions, hover info, diagnostics, Monaco Editor | +| [Migration Guide](docs/migration.md) | Upgrading from original expr-eval or previous versions | + +### For Contributors + | Document | Description | |:---------|:------------| -| [Parser](docs/parser.md) | Parser class API, constructor options, and methods | -| [Expression](docs/expression.md) | Expression object methods: evaluate, substitute, simplify, variables, symbols, toString, toJSFunction | -| [Expression Syntax](docs/syntax.md) | Operator precedence, unary operators, pre-defined functions, string manipulation, array literals, function definitions, constants | -| [TypeScript Port Enhancements](docs/enhancements.md) | New features: undefined support, coalesce operator, optional chaining, SQL case blocks, object construction, promises, and more | -| [Language Service](docs/language-service.md) | IDE integration: code completions, hover information, syntax highlighting, Monaco Editor integration | -| [Performance Testing](docs/performance.md) | Benchmarks, performance grades, and optimization guidance | +| [Contributing](CONTRIBUTING.md) | Development setup, code style, and PR guidelines | +| [Performance Testing](docs/performance.md) | Benchmarks, profiling, and optimization guidance | +| [Breaking Changes](BREAKING_CHANGES.md) | Version-by-version breaking change documentation | ## Key Features diff --git a/docs/advanced-features.md b/docs/advanced-features.md new file mode 100644 index 00000000..b34f8d43 --- /dev/null +++ b/docs/advanced-features.md @@ -0,0 +1,266 @@ +# Advanced Features + +> **Audience:** Developers integrating expr-eval who need advanced customization and features. + +This document covers advanced integration features beyond basic parsing and evaluation. For expression syntax, see [Expression Syntax](syntax.md). For basic parser usage, see [Parser](parser.md). + +## About This TypeScript Port + +This is a modern TypeScript port of the expr-eval library, completely rewritten with contemporary build tools and development practices. Originally based on [expr-eval 2.0.2](http://silentmatt.com/javascript-expression-evaluator/), this version has been restructured with a modular architecture, TypeScript support, and comprehensive testing using Vitest. + +The library maintains backward compatibility while providing enhanced features and improved maintainability. + +## Async Expressions (Promise Support) + +Custom functions can return promises. When they do, `evaluate()` returns a promise: + +```js +const parser = new Parser(); + +// Synchronous function +parser.functions.double = value => value * 2; +parser.evaluate('double(2) + 3'); // 7 + +// Async function +parser.functions.fetchData = async (id) => { + const response = await fetch(`/api/data/${id}`); + return response.json(); +}; + +// evaluate() now returns a Promise +const result = await parser.evaluate('fetchData(123) + 10'); +``` + +**Note:** When any function in an expression returns a promise, the entire `evaluate()` call becomes async. + +## Custom Variable Name Resolution + +The `parser.resolve` callback is called when a variable name is not found in the provided variables object. This enables: + +- Variable name aliasing +- Dynamic variable lookup +- Custom naming conventions (e.g., `$variable` syntax) + +```js +const parser = new Parser(); + +// Example 1: Alias resolution +const data = { variables: { a: 5, b: 10 } }; + +parser.resolve = (name) => { + if (name === '$v') { + return { alias: 'variables' }; + } + return undefined; +}; + +parser.evaluate('$v.a + $v.b', data); // 15 + +// Example 2: Direct value resolution +parser.resolve = (name) => { + if (name.startsWith('$')) { + const key = name.substring(1); + return { value: data.variables[key] }; + } + return undefined; +}; + +parser.evaluate('$a + $b', {}); // 15 +``` + +**Return values:** +- `{ alias: string }` - Redirect to another variable name +- `{ value: any }` - Return a value directly +- `undefined` - Use default behavior (throws error for unknown variables) + +## Type Conversion (as Operator) + +The `as` operator provides type conversion capabilities. **Disabled by default.** + +```js +const parser = new Parser({ operators: { conversion: true } }); + +parser.evaluate('"1.6" as "number"'); // 1.6 +parser.evaluate('"1.6" as "int"'); // 2 (rounded) +parser.evaluate('"1.6" as "integer"'); // 2 (rounded) +parser.evaluate('"1" as "boolean"'); // true +parser.evaluate('"" as "boolean"'); // false +``` + +### Custom Type Conversion + +Override `parser.binaryOps.as` to implement custom type conversion: + +```js +const parser = new Parser({ operators: { conversion: true } }); + +// Integrate with a date library +parser.binaryOps.as = (value, type) => { + if (type === 'date') { + return new Date(value); + } + if (type === 'currency') { + return `$${Number(value).toFixed(2)}`; + } + // Fall back to default behavior + return defaultAsOperator(value, type); +}; + +parser.evaluate('"2024-01-15" as "date"'); // Date object +parser.evaluate('1234.5 as "currency"'); // "$1234.50" +``` + +## Expression Syntax Features + +The following syntax features are available in expressions. They are documented here for developers to understand what's available; users should refer to [Expression Syntax](syntax.md). + +### Undefined Support + +The `undefined` keyword is available in expressions: + +```js +x > 3 ? undefined : x +x == undefined ? 1 : 2 +``` + +**Behavior:** +- Variables can be set to `undefined` without errors +- Most operators return `undefined` if any operand is `undefined`: `2 + undefined` → `undefined` +- Comparison operators follow JavaScript semantics: `3 > undefined` → `false` + +### Coalesce Operator (??) + +The `??` operator returns the right operand when the left is: +- `undefined` +- `null` +- `Infinity` (e.g., division by zero) +- `NaN` + +``` +x ?? 0 // Returns 0 if x is null/undefined +10 / 0 ?? -1 // Returns -1 (10/0 is Infinity) +sqrt(-1) ?? 0 // Returns 0 (sqrt(-1) is NaN) +``` + +### Optional Chaining for Property Access + +Property access automatically handles missing properties without throwing errors: + +```js +const obj = { user: { profile: { name: 'Ada' } } }; + +parser.evaluate('user.profile.name', obj); // 'Ada' +parser.evaluate('user.profile.email', obj); // undefined (not error) +parser.evaluate('user.settings.theme', obj); // undefined (not error) +parser.evaluate('user.settings.theme ?? "dark"', obj); // 'dark' +``` + +### Not In Operator + +The `not in` operator checks if a value is not in an array: + +``` +"d" not in ["a", "b", "c"] // true +"a" not in ["a", "b", "c"] // false +``` + +Equivalent to: `not ("a" in ["a", "b", "c"])` + +**Note:** Requires `operators.in: true` in parser options. + +### String Concatenation with + + +The `+` operator concatenates strings: + +``` +"hello" + " " + "world" // "hello world" +"Count: " + 42 // "Count: 42" +``` + +### SQL-Style CASE Blocks + +SQL-style CASE expressions provide multi-way conditionals: + +**Switch-style (comparing a value):** + +``` +case status + when "active" then "✓ Active" + when "pending" then "⏳ Pending" + when "inactive" then "✗ Inactive" + else "Unknown" +end +``` + +**If/else-style (evaluating conditions):** + +``` +case + when score >= 90 then "A" + when score >= 80 then "B" + when score >= 70 then "C" + when score >= 60 then "D" + else "F" +end +``` + +> **Note:** `toJSFunction()` is not supported for expressions that use CASE blocks. + +### Object Construction + +Create objects directly in expressions: + +``` +{ + name: firstName + " " + lastName, + age: currentYear - birthYear, + scores: [test1, test2, test3], + meta: { + created: now, + version: 1 + } +} +``` + +### json() Function + +Convert values to JSON strings: + +``` +json([1, 2, 3]) // "[1,2,3]" +json({a: 1, b: 2}) // '{"a":1,"b":2}' +``` + +## Operator Customization + +### Custom Binary Operators + +Add or modify binary operators via `parser.binaryOps`: + +```js +const parser = new Parser(); + +// Positive modulo (always returns positive) +parser.binaryOps['%%'] = (a, b) => ((a % b) + b) % b; + +// String repeat operator +parser.binaryOps['**'] = (str, n) => str.repeat(n); +``` + +### Custom Unary Operators + +Add or modify unary operators via `parser.unaryOps`: + +```js +const parser = new Parser(); + +// Custom unary operator +parser.unaryOps['$'] = (x) => `$${x.toFixed(2)}`; +``` + +## See Also + +- [Parser](parser.md) - Parser configuration and methods +- [Expression](expression.md) - Expression object API +- [Expression Syntax](syntax.md) - Complete syntax reference for expression writers +- [Language Service](language-service.md) - IDE integration diff --git a/docs/enhancements.md b/docs/enhancements.md index eebbb688..9159fda6 100644 --- a/docs/enhancements.md +++ b/docs/enhancements.md @@ -1,233 +1,31 @@ # TypeScript Port Enhancements -This is a modern TypeScript port of the expr-eval library, completely rewritten with contemporary build tools and development practices. Originally based on [expr-eval 2.0.2](http://silentmatt.com/javascript-expression-evaluator/), this version has been restructured with a modular architecture, TypeScript support, and comprehensive testing using Vitest. The library almost maintains backward compatibility while providing enhanced features and improved maintainability. +> **Note:** This document has been reorganized. See [Advanced Features](advanced-features.md) for the main documentation. -This port adds the following enhancements over the original: +This is a modern TypeScript port of the expr-eval library, completely rewritten with contemporary build tools and development practices. Originally based on [expr-eval 2.0.2](http://silentmatt.com/javascript-expression-evaluator/), this version has been restructured with a modular architecture, TypeScript support, and comprehensive testing using Vitest. The library maintains backward compatibility while providing enhanced features and improved maintainability. -## Support for json() function +## Summary of Enhancements -This will return a JSON string: +This TypeScript port adds the following features over the original library: -```js -json([1, 2, 3]) -``` +### Expression Syntax Enhancements -## Support for undefined +- **`undefined` keyword** - Use `undefined` in expressions and handle undefined values gracefully +- **Coalesce operator (`??`)** - Null/undefined fallback: `x ?? defaultValue` +- **`not in` operator** - Check if value is not in array: `"x" not in arr` +- **Optional chaining** - Property access returns `undefined` instead of throwing errors +- **String concatenation with `+`** - Concatenate strings using the `+` operator +- **SQL-style CASE blocks** - Multi-way conditionals with `case/when/then/else/end` +- **Object construction** - Create objects with `{key: value}` syntax +- **`json()` function** - Convert values to JSON strings -The concept of JavaScript's undefined has been added to the parser. +### Developer Integration Features -### undefined keyword +- **Promise support** - Custom functions can return promises (async evaluation) +- **Custom variable resolution** - `parser.resolve` callback for dynamic variable lookup +- **`as` operator** - Type conversion with customizable implementation -The undefined keyword has been added to the parser allowing it to be used in expressions. - -```js -x > 3 ? undefined : x -x == undefined ? 1 : 2 -``` - -### Setting expression variables to undefined - -If you set a local variable to undefined, expr-eval would generate an error saying that your variable was unrecognized. - -For example: - -```js -/* myCustomFn() returns undefined */ -x = myCustomFn(); x > 3 -/* Error: unrecognized variable: x */ -``` - -This has been fixed, you can now set expression variables to undefined and they will resolve correctly. - -### Operators/functions gracefully support undefined - -All operators and built-in functions have been extended to gracefully support undefined. Generally speaking if one of the input values is undefined then the operator/function returns undefined. So `2 + undefined` is `undefined`, `max(0, 1, undefined)` is `undefined`, etc. - -Logical operators act just like JavaScript, so `3 > undefined` is `false`. - -## Coalesce Operator - -The coalesce operator `??` has been added; `x ?? y` will evaluate to y if x is: - -* `undefined` -* `null` -* Infinity (divide by zero) -* NaN - -Examples: - -```js -var parser = new Parser(); -var obj = { x: undefined, y: 10, z: 0 }; -parser.evaluate('x ?? 0', obj); // 0 -parser.evaluate('y ?? 0', obj); // 10 -parser.evaluate('x ?? 1 * 3', obj); // (undefined ?? 1) * 3 = 3 -parser.evaluate('y ?? 1 * 3', obj); // (10 ?? 1) * 3 = 30 -parser.evaluate('10 / z', obj); // Infinity -parser.evaluate('10 / z ?? 0', obj); // 0 -parser.evaluate('sqrt -1'); // NaN -parser.evaluate('sqrt -1 ?? 0'); // 0 -``` - -## Not In Operator - -The `not in` operator has been added. - -`"a" not in ["a", "b", "c"]` - -is equivalent to - -`not ("a" in ["a", "b", "c"])` - -## Optional Chaining for Property Access - -Structure/array property references now act like `?.`, meaning if the entire property chain does not exist then instead of throwing an error the value of the property is undefined. - -For example: - -```js -var parser = new Parser(); -var obj = { thingy: { array: [{ value: 10 }] } }; -parser.evaluate('thingy.array[0].value', obj); // 10 -parser.evaluate('thingy.array[1].value', obj); // undefined -parser.evaluate('thingy.doesNotExist[0].childArray[1].notHere.alsoNotHere', obj); // undefined -parser.evaluate('thingy.array[0].value.doesNotExist', obj); // undefined -``` - -This can be combined with the coalesce operator to gracefully fall back on a default value if some part of a long property reference is `undefined`. - -```js -var parser = new Parser(); -var obj = { thingy: { array: [{ value: 10 }] } }; -parser.evaluate('thingy.array[1].value ?? 0', obj); // 0 -``` - -## String Concatenation Using + - -The + operator can now be used to concatenate strings. - -```js -var parser = new Parser(); -var obj = { thingy: { array: [{ value: 10 }] } }; -parser.evaluate('"abc" + "def" + "ghi"', obj); // 'abcdefghi' -``` - -## Support for Promises in Custom Functions - -Custom functions can return promises. When this happens evaluate will return a promise that when resolved contains the expression value. - -```js -const parser = new Parser(); - -parser.functions.doIt = value => value + value; -parser.evaluate('doIt(2) + 3'); // 7 - -parser.functions.doIt = value => - new Promise((resolve) => setTimeout(() => resolve(value + value), 100)); -await parser.evaluate('doIt(2) + 3'); // 7 -``` - -## Support for Custom Variable Name Resolution - -Custom logic can be provided to resolve unrecognized variable names. The parser has a resolve callback that will be called any time a variable name is not recognized. This can return an object that either indicates that the variable name is an alias for another variable or it can return the variable value. - -```js -const parser = new Parser(); -const obj = { variables: { a: 5, b: 1 } }; -parser.resolve = token => token === '$v' ? { alias: 'variables' } : undefined; -parser.evaluate('$v.a + variables.b', obj); // 6 - -parser.resolve = token => - token.startsWith('$') ? { value: obj.variables[token.substring(1)] } : undefined; -assert.strictEqual(parser.evaluate('$a + $b'), 6); -``` - -## SQL Style Case Blocks - -> **NOTE:** `toJSFunction()` is not supported for expressions that use case blocks. - -SQL style case blocks are now supported, for both cases which evaluate a value against other values (a switch style case) and cases which test for the first truthy when (if/else/if style cases). - -### Switch-style case - -```js -const parser = new Parser(); -const expr = ` - case x - when 1 then 'one' - when 1+1 then 'two' - when 1+1+1 then 'three' - else 'too-big' - end -`; -parser.evaluate(expr, { x: 1 }); // 'one' -parser.evaluate(expr, { x: 2 }); // 'two' -parser.evaluate(expr, { x: 3 }); // 'three' -parser.evaluate(expr, { x: 4 }); // 'too-big' -``` - -### If/else-style case - -```js -const parser = new Parser(); -const expr = ` - case - when x == 1 then 'one' - when x == 1+1 then 'two' - when x == 1+1+1 then 'three' - else 'too-big' - end -`; -parser.evaluate(expr, { x: 1 }); // 'one' -parser.evaluate(expr, { x: 2 }); // 'two' -parser.evaluate(expr, { x: 3 }); // 'three' -parser.evaluate(expr, { x: 4 }); // 'too-big' -``` - -## Object Construction - -Objects can be created using JavaScript syntax. This allows for expressions that return object values and for object arguments to be passed to custom functions. - -```js -const parser = new Parser(); -const expr = `{ - a: x * 3, - b: { - /*this x is a property and not the x on the input object*/ - x: "first" + "_" + "second", - y: min(x, 0), - }, - c: [0, 1, 2, x], -}`; -parser.evaluate(expr, { x: 3 }); -/* -{ - a: 15, - b: { - x: 'first_second', - z: 0 - }, - c: [0, 1, 2, 3] -} -*/ -``` - -## As Operator (Type Conversion) - -An as operator has been added to support type conversion. **This operator is disabled by default and must be explicitly enabled by setting `operators.conversion` to true in the options.** It can be used to perform value conversion. By default is of limited value; it only supports converting values to numbers, int/integer (by rounding the number), and boolean. The intent is to allow integration of more sophisticated value conversion packages such as numeral.js and moment for conversion of other values. - -```js -const parser = new Parser({ operators: { conversion: true } }); -parser.evaluate('"1.6" as "number"'); // 1.6 -parser.evaluate('"1.6" as "int"'); // 2 -parser.evaluate('"1.6" as "integer"'); // 2 -parser.evaluate('"1.6" as "boolean"'); // true -``` - -The default `as` implementation can be overridden by replacing `parser.binaryOps.as`. - -```js -const parser = new Parser({ operators: { conversion: true } }); -parser.binaryOps.as = (a, _b) => a + '_suffix'; -parser.evaluate('"abc" as "suffix"'); // 'abc_suffix' -``` +For detailed documentation, see: +- [Advanced Features](advanced-features.md) - Developer integration features +- [Expression Syntax](syntax.md) - Complete syntax reference +- [Parser](parser.md) - Parser configuration diff --git a/docs/expression.md b/docs/expression.md index ec4d8e31..81539655 100644 --- a/docs/expression.md +++ b/docs/expression.md @@ -1,5 +1,7 @@ # Expression +> **Audience:** Developers integrating expr-eval into their projects. + `Parser.parse(str)` returns an `Expression` object. `Expression`s are similar to JavaScript functions, i.e. they can be "called" with variables bound to passed-in values. In fact, they can even be converted into JavaScript functions. ## evaluate(variables?: object) @@ -7,10 +9,10 @@ Evaluate the expression, with variables bound to the values in `{variables}`. Each variable in the expression is bound to the corresponding member of the `variables` object. If there are unbound variables, `evaluate` will throw an exception. ```js -js> expr = Parser.parse("2 ^ x"); -(2^x) -js> expr.evaluate({ x: 3 }); -8 +import { Parser } from '@pro-fa/expr-eval'; + +const expr = Parser.parse("2 ^ x"); +console.log(expr.evaluate({ x: 3 })); // 8 ``` ## substitute(variable: string, expression: Expression | string | number) @@ -18,12 +20,12 @@ js> expr.evaluate({ x: 3 }); Create a new `Expression` with the specified variable replaced with another expression. This is similar to function composition. If `expression` is a string or number, it will be parsed into an `Expression`. ```js -js> expr = Parser.parse("2 * x + 1"); -((2*x)+1) -js> expr.substitute("x", "4 * x"); -((2*(4*x))+1) -js> expr2.evaluate({ x: 3 }); -25 +const expr = Parser.parse("2 * x + 1"); +console.log(expr.toString()); // ((2*x)+1) + +const expr2 = expr.substitute("x", "4 * x"); +console.log(expr2.toString()); // ((2*(4*x))+1) +console.log(expr2.evaluate({ x: 3 })); // 25 ``` ## simplify(variables: object) @@ -33,10 +35,9 @@ Simplify constant sub-expressions and replace variable references with literal v Simplify is pretty simple. For example, it doesn't know that addition and multiplication are associative, so `((2*(4*x))+1)` from the previous example cannot be simplified unless you provide a value for x. `2*4*x+1` can however, because it's parsed as `(((2*4)*x)+1)`, so the `(2*4)` sub-expression will be replaced with "8", resulting in `((8*x)+1)`. ```js -js> expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 }); -(x*3.141592653589793) -js> expr.evaluate({ x: 2 }); -6.283185307179586 +const expr = Parser.parse("x * (y * atan(1))").simplify({ y: 4 }); +console.log(expr.toString()); // (x*3.141592653589793) +console.log(expr.evaluate({ x: 2 })); // 6.283185307179586 ``` ## variables(options?: object) @@ -44,12 +45,9 @@ js> expr.evaluate({ x: 2 }); Get an array of the unbound variables in the expression. ```js -js> expr = Parser.parse("x * (y * atan(1))"); -(x*(y*atan(1))) -js> expr.variables(); -x,y -js> expr.simplify({ y: 4 }).variables(); -x +const expr = Parser.parse("x * (y * atan(1))"); +console.log(expr.variables()); // ['x', 'y'] +console.log(expr.simplify({ y: 4 }).variables()); // ['x'] ``` By default, `variables` will return "top-level" objects, so for example, `Parser.parse(x.y.z).variables()` returns `['x']`. If you want to get the whole chain of object members, you can call it with `{ withMembers: true }`. So `Parser.parse(x.y.z).variables({ withMembers: true })` would return `['x.y.z']`. @@ -59,12 +57,9 @@ By default, `variables` will return "top-level" objects, so for example, `Parser Get an array of variables, including any built-in functions used in the expression. ```js -js> expr = Parser.parse("min(x, y, z)"); -(min(x, y, z)) -js> expr.symbols(); -min,x,y,z -js> expr.simplify({ y: 4, z: 5 }).symbols(); -min,x +const expr = Parser.parse("min(x, y, z)"); +console.log(expr.symbols()); // ['min', 'x', 'y', 'z'] +console.log(expr.simplify({ y: 4, z: 5 }).symbols()); // ['min', 'x'] ``` Like `variables`, `symbols` accepts an option argument `{ withMembers: true }` to include object members. @@ -80,14 +75,13 @@ Convert an `Expression` object into a callable JavaScript function. `parameters` If the optional `variables` argument is provided, the expression will be simplified with variables bound to the supplied values. ```js -js> expr = Parser.parse("x + y + z"); -((x + y) + z) -js> f = expr.toJSFunction("x,y,z"); -[Function] // function (x, y, z) { return x + y + z; }; -js> f(1, 2, 3) -6 -js> f = expr.toJSFunction("y,z", { x: 100 }); -[Function] // function (y, z) { return 100 + y + z; }; -js> f(2, 3) -105 +const expr = Parser.parse("x + y + z"); + +// Create a function with all three parameters +const f1 = expr.toJSFunction("x,y,z"); +console.log(f1(1, 2, 3)); // 6 + +// Create a function with x pre-bound to 100 +const f2 = expr.toJSFunction("y,z", { x: 100 }); +console.log(f2(2, 3)); // 105 ``` diff --git a/docs/language-service.md b/docs/language-service.md index ac6440ae..aafbc9e3 100644 --- a/docs/language-service.md +++ b/docs/language-service.md @@ -1,5 +1,7 @@ # Language Service +> **Audience:** Developers building IDE integrations or code editors with expr-eval support. + The library includes a built-in language service that provides IDE-like features for expr-eval expressions. This is useful for integrating expr-eval into code editors like Monaco Editor (used by VS Code). ## Features diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..2848f365 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,167 @@ +# Migration Guide + +> **Audience:** Developers upgrading from the original `expr-eval` library or previous versions. + +This guide helps you migrate to the current version of `@pro-fa/expr-eval`. + +## Migrating from silentmatt/expr-eval + +This library is a TypeScript port of the original [expr-eval](https://github.com/silentmatt/expr-eval) library. Most expressions will work without changes, but there are some differences to be aware of. + +### What's the Same + +- Core expression syntax (arithmetic, comparison, logical operators) +- Built-in math functions (sin, cos, sqrt, etc.) +- Expression methods (evaluate, simplify, variables, toJSFunction) +- Parser configuration for enabling/disabling operators + +### What's New + +| Feature | Description | +|---------|-------------| +| `undefined` keyword | Use `undefined` in expressions | +| Coalesce operator (`??`) | Null/undefined fallback | +| Optional chaining | Property access returns `undefined` instead of errors | +| `not in` operator | Check if value is not in array | +| SQL CASE blocks | Multi-way conditionals | +| Object construction | Create objects with `{key: value}` | +| Arrow functions | `x => x * 2` syntax | +| Promise support | Async custom functions | +| String concatenation with `+` | `"a" + "b"` works | +| Language service | IDE integration (completions, hover, diagnostics) | + +### Behavior Changes + +#### Undefined Handling + +The original library would throw errors for undefined values. This version handles them gracefully: + +```js +// Original: throws error +// New: returns undefined +parser.evaluate('x + 1', { x: undefined }); // undefined + +// Use coalesce for fallback +parser.evaluate('x ?? 0 + 1', { x: undefined }); // 1 +``` + +#### Property Access + +Missing properties now return `undefined` instead of throwing: + +```js +const obj = { user: { name: 'Ada' } }; + +// Original: throws error +// New: returns undefined +parser.evaluate('user.email', obj); // undefined +parser.evaluate('user.profile.photo', obj); // undefined +``` + +## Migrating Between Major Versions + +### Version 6.0.0 + +**`null` comparison changed:** + +```js +// Before 6.0: null was cast to 0 +null == 0 // true (before) +null == 0 // false (after) + +// Now null equals null +null == someNullVariable // true (after) +``` + +### Version 5.0.0 + +**Critical security change: Functions must be registered explicitly.** + +This addresses several security vulnerabilities (CVE-2025-12735, CVE-2025-13204). + +```js +// BEFORE (vulnerable, no longer works) +parser.evaluate('customFunc()', { customFunc: () => 'result' }); +parser.evaluate('obj.method()', { obj: { method: () => 'danger' } }); + +// AFTER (secure) +parser.functions.customFunc = () => 'result'; +parser.evaluate('customFunc()'); +``` + +**What still works:** +- Passing primitive values via context +- Passing objects with non-function properties +- Built-in functions +- Inline function definitions: `(f(x) = x * 2)(5)` +- Functions registered in `parser.functions` + +**Migration steps:** + +1. Search your code for `evaluate('...', { fn: ... })` patterns where `fn` is a function +2. Move those functions to `parser.functions`: + +```js +// Before +const myFunc = (x) => x * 2; +parser.evaluate('myFunc(5)', { myFunc }); + +// After +parser.functions.myFunc = (x) => x * 2; +parser.evaluate('myFunc(5)'); +``` + +**Protected properties:** + +Access to these properties is now blocked: +- `__proto__` +- `prototype` +- `constructor` + +See [BREAKING_CHANGES.md](BREAKING_CHANGES.md) for complete details. + +## Package Name Change + +If you're migrating from the original package: + +```bash +# Remove old package +npm uninstall expr-eval + +# Install new package +npm install @pro-fa/expr-eval +``` + +Update imports: + +```js +// Before +const { Parser } = require('expr-eval'); + +// After +import { Parser } from '@pro-fa/expr-eval'; +// or +const { Parser } = require('@pro-fa/expr-eval'); +``` + +## TypeScript Support + +This version includes full TypeScript type definitions. If you were using `@types/expr-eval`, you can remove it: + +```bash +npm uninstall @types/expr-eval +``` + +Types are exported from the main package: + +```typescript +import { Parser, Expression, Value, Values } from '@pro-fa/expr-eval'; +``` + +## Getting Help + +If you encounter issues during migration: + +1. Check the [BREAKING_CHANGES.md](BREAKING_CHANGES.md) for detailed breaking change information +2. Review the [documentation](docs/) for the feature you're using +3. Open an issue on GitHub with a minimal reproduction case diff --git a/docs/parser.md b/docs/parser.md index bdc5cf9e..c1088121 100644 --- a/docs/parser.md +++ b/docs/parser.md @@ -1,50 +1,249 @@ # Parser -Parser is the main class in the library. It has a single `parse` method, and "static" methods for parsing and evaluating expressions. +> **Audience:** Developers integrating expr-eval into their projects. -## Parser() +The `Parser` class is the main entry point for parsing and evaluating expressions. -Constructs a new `Parser` instance. +## Quick Start -The constructor takes an optional `options` parameter that allows you to enable or disable operators. +```js +import { Parser } from '@pro-fa/expr-eval'; + +// Create a parser instance +const parser = new Parser(); + +// Parse and evaluate in one step +const result = Parser.evaluate('2 * x + 1', { x: 3 }); // 7 + +// Or parse once and evaluate multiple times +const expr = parser.parse('2 * x + 1'); +expr.evaluate({ x: 3 }); // 7 +expr.evaluate({ x: 5 }); // 11 +``` + +## Constructor + +```js +const parser = new Parser(options?); +``` + +### Options + +| Option | Type | Description | +|:-------|:-----|:------------| +| `operators` | `object` | Enable/disable specific operators (see table below) | -For example, the following will create a `Parser` that does not allow comparison or logical operators, but does allow `in`: +### Operator Configuration + +All operators default to `true` (enabled) except where noted: + +| Option Key | Default | Operators Affected | +|:-----------|:--------|:-------------------| +| `add` | `true` | `+` (addition) | +| `subtract` | `true` | `-` (subtraction) | +| `multiply` | `true` | `*` (multiplication) | +| `divide` | `true` | `/` (division) | +| `remainder` | `true` | `%` (modulo) | +| `power` | `true` | `^` (exponentiation) | +| `factorial` | `true` | `!` (factorial) | +| `concatenate` | `true` | `\|` (array/string concatenation) | +| `conditional` | `true` | `? :` (ternary) and `??` (coalesce) | +| `logical` | `true` | `and`, `or`, `not` | +| `comparison` | `true` | `==`, `!=`, `<`, `>`, `<=`, `>=` | +| `in` | **`false`** | `in`, `not in` | +| `assignment` | **`false`** | `=` (variable assignment) | +| `fndef` | `true` | Function definitions and arrow functions | +| `conversion` | **`false`** | `as` (type conversion) | + +**Example: Restricted Parser** ```js const parser = new Parser({ operators: { - // These default to true, but are included to be explicit - add: true, - concatenate: true, - conditional: true, - divide: true, - factorial: true, - multiply: true, - power: true, - remainder: true, - subtract: true, - - // Disable and, or, not, <, ==, !=, etc. + // Disable logical and comparison logical: false, comparison: false, - - // Disable 'in' and = operators - 'in': false, - assignment: false + + // Enable 'in' operator + in: true, + + // Enable assignment + assignment: true } }); ``` -## parse(expression: string) +## Instance Methods -Convert a mathematical expression into an `Expression` object. +### parse(expression: string) -## Parser.parse(expression: string) +Convert an expression string into an `Expression` object. + +```js +const expr = parser.parse('x * 2 + y'); +``` + +Returns an [Expression](expression.md) object with methods like `evaluate()`, `simplify()`, `variables()`, etc. + +### evaluate(expression: string, variables?: object) + +Parse and immediately evaluate an expression. + +```js +parser.evaluate('x + y', { x: 2, y: 3 }); // 5 +``` + +## Static Methods + +### Parser.parse(expression: string) Static equivalent of `new Parser().parse(expression)`. -## Parser.evaluate(expression: string, variables?: object) +```js +const expr = Parser.parse('x + 1'); +``` + +### Parser.evaluate(expression: string, variables?: object) + +Parse and immediately evaluate an expression. Equivalent to `Parser.parse(expr).evaluate(vars)`. + +```js +Parser.evaluate('6 * x', { x: 7 }); // 42 +``` + +## Instance Properties + +### parser.functions + +An object containing all available functions. You can add, modify, or remove functions: + +```js +const parser = new Parser(); + +// Add a custom function +parser.functions.double = (x) => x * 2; + +// Add a function that returns a Promise (makes evaluate async) +parser.functions.fetchValue = async (id) => { + const response = await fetch(`/api/values/${id}`); + return response.json(); +}; + +// Remove a built-in function +delete parser.functions.random; + +// Use custom function +parser.evaluate('double(5)'); // 10 + +// Async evaluation +await parser.evaluate('fetchValue(123) * 2'); +``` + +### parser.consts + +An object containing all available constants. You can add, modify, or remove constants: + +```js +const parser = new Parser(); + +// Add custom constants +parser.consts.TAU = Math.PI * 2; +parser.consts.GOLDEN_RATIO = 1.618033988749; + +// Use in expressions +parser.evaluate('TAU'); // 6.283185307179586 +parser.evaluate('2 * PI'); // 6.283185307179586 (PI is built-in) + +// Remove all built-in constants +parser.consts = {}; +``` + +**Built-in Constants:** + +| Constant | Value | +|:---------|:------| +| `E` | `Math.E` (~2.718) | +| `PI` | `Math.PI` (~3.14159) | +| `true` | `true` | +| `false` | `false` | + +### parser.resolve + +A callback for custom variable name resolution. Called when a variable name is not found in the provided variables object. + +```js +const parser = new Parser(); +const data = { variables: { a: 5, b: 10 } }; + +// Alias resolution: $v becomes 'variables' +parser.resolve = (name) => { + if (name === '$v') { + return { alias: 'variables' }; + } + return undefined; +}; + +parser.evaluate('$v.a + $v.b', data); // 15 + +// Value resolution: return the value directly +parser.resolve = (name) => { + if (name.startsWith('$')) { + const key = name.substring(1); + return { value: data.variables[key] }; + } + return undefined; +}; + +parser.evaluate('$a + $b', {}); // 15 +``` + +The `resolve` callback should return: +- `{ alias: string }` - to redirect to another variable name +- `{ value: any }` - to return a value directly +- `undefined` - to use default behavior (throws error for unknown variables) + +## Advanced Configuration + +### Type Conversion (as operator) + +The `as` operator is disabled by default. When enabled, it provides basic type conversion: + +```js +const parser = new Parser({ operators: { conversion: true } }); + +parser.evaluate('"1.6" as "number"'); // 1.6 +parser.evaluate('"1.6" as "int"'); // 2 (rounded) +parser.evaluate('"1.6" as "integer"'); // 2 (rounded) +parser.evaluate('"1" as "boolean"'); // true +``` + +You can override the default `as` implementation: + +```js +parser.binaryOps.as = (value, type) => { + // Custom conversion logic + if (type === 'date') { + return new Date(value); + } + return value; +}; +``` + +### Operator Customization + +You can customize binary and unary operators via `parser.binaryOps` and `parser.unaryOps`: + +```js +const parser = new Parser(); + +// Add a custom binary operator +parser.binaryOps['%%'] = (a, b) => ((a % b) + b) % b; // Positive modulo + +// Note: Custom operators must be registered before parsing +``` -Parse and immediately evaluate an expression using the values and functions from the `variables` object. +## See Also -`Parser.evaluate(expr, vars)` is equivalent to calling `Parser.parse(expr).evaluate(vars)`. +- [Expression](expression.md) - Expression object methods +- [Expression Syntax](syntax.md) - Complete syntax reference +- [Advanced Features](advanced-features.md) - Promises, SQL CASE, object construction diff --git a/docs/performance.md b/docs/performance.md index ccc85019..62177563 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,5 +1,7 @@ # Performance Testing Guide +> **Audience:** Contributors working on expr-eval internals and performance optimization. + This document explains how to run and interpret performance tests for the expr-eval library. ## Overview diff --git a/docs/quick-reference.md b/docs/quick-reference.md new file mode 100644 index 00000000..3cd5004a --- /dev/null +++ b/docs/quick-reference.md @@ -0,0 +1,163 @@ +# Expression Quick Reference + +> **Audience:** Users writing expressions in applications powered by expr-eval. + +This is a quick reference card. For detailed documentation, see [Expression Syntax](syntax.md). + +## Arithmetic + +| Expression | Result | Description | +|:-----------|:-------|:------------| +| `2 + 3` | 5 | Addition | +| `10 - 4` | 6 | Subtraction | +| `3 * 4` | 12 | Multiplication | +| `15 / 3` | 5 | Division | +| `10 % 3` | 1 | Remainder (modulo) | +| `2 ^ 3` | 8 | Exponentiation | +| `5!` | 120 | Factorial | +| `-x` | negated | Negation | + +## Comparison + +| Expression | Result | Description | +|:-----------|:-------|:------------| +| `5 > 3` | true | Greater than | +| `5 >= 5` | true | Greater than or equal | +| `3 < 5` | true | Less than | +| `3 <= 3` | true | Less than or equal | +| `5 == 5` | true | Equal | +| `5 != 3` | true | Not equal | +| `"a" in ["a", "b"]` | true | In array (may be disabled) | + +## Logic + +| Expression | Result | Description | +|:-----------|:-------|:------------| +| `true and false` | false | Logical AND | +| `true or false` | true | Logical OR | +| `not true` | false | Logical NOT | + +## Conditionals + +| Expression | Result | Description | +|:-----------|:-------|:------------| +| `x > 0 ? "yes" : "no"` | depends on x | Ternary (if-then-else) | +| `x ?? 0` | x or 0 | Coalesce (null/undefined fallback) | + +## Math Functions + +| Function | Example | Result | +|:---------|:--------|:-------| +| `abs(x)` | `abs(-5)` | 5 | +| `round(x)` | `round(3.7)` | 4 | +| `floor(x)` | `floor(3.7)` | 3 | +| `ceil(x)` | `ceil(3.2)` | 4 | +| `sqrt(x)` | `sqrt(16)` | 4 | +| `min(a, b, ...)` | `min(3, 1, 4)` | 1 | +| `max(a, b, ...)` | `max(3, 1, 4)` | 4 | +| `clamp(x, min, max)` | `clamp(15, 0, 10)` | 10 | +| `pow(x, y)` | `pow(2, 3)` | 8 | +| `sin(x)` | `sin(PI / 2)` | 1 | +| `cos(x)` | `cos(0)` | 1 | +| `log(x)` | `log(E)` | 1 | +| `log10(x)` | `log10(100)` | 2 | + +## String Functions + +| Function | Example | Result | +|:---------|:--------|:-------| +| `length(s)` | `length("hello")` | 5 | +| `toUpper(s)` | `toUpper("hi")` | "HI" | +| `toLower(s)` | `toLower("HI")` | "hi" | +| `trim(s)` | `trim(" x ")` | "x" | +| `left(s, n)` | `left("hello", 2)` | "he" | +| `right(s, n)` | `right("hello", 2)` | "lo" | +| `contains(s, sub)` | `contains("hello", "ell")` | true | +| `startsWith(s, sub)` | `startsWith("hello", "he")` | true | +| `endsWith(s, sub)` | `endsWith("hello", "lo")` | true | +| `replace(s, old, new)` | `replace("aa", "a", "b")` | "bb" | +| `split(s, delim)` | `split("a,b", ",")` | ["a", "b"] | + +## Array Functions + +| Function | Example | Result | +|:---------|:--------|:-------| +| `count(arr)` | `count([1, 2, 3])` | 3 | +| `indexOf(val, arr)` | `indexOf(2, [1, 2, 3])` | 1 | +| `join(sep, arr)` | `join("-", [1, 2])` | "1-2" | +| `unique(arr)` | `unique([1, 1, 2])` | [1, 2] | +| `map(arr, fn)` | `map([1, 2], x => x * 2)` | [2, 4] | +| `filter(arr, fn)` | `filter([1, 2, 3], x => x > 1)` | [2, 3] | +| `find(arr, fn)` | `find([1, 5, 2], x => x > 3)` | 5 | +| `fold(arr, init, fn)` | `fold([1, 2, 3], 0, (a, x) => a + x)` | 6 | +| `some(arr, fn)` | `some([1, 5], x => x > 3)` | true | +| `every(arr, fn)` | `every([1, 2], x => x > 0)` | true | + +## Object Functions + +| Function | Example | Result | +|:---------|:--------|:-------| +| `keys(obj)` | `keys({a: 1, b: 2})` | ["a", "b"] | +| `values(obj)` | `values({a: 1, b: 2})` | [1, 2] | +| `merge(o1, o2)` | `merge({a: 1}, {b: 2})` | {a: 1, b: 2} | + +## Type Checking + +| Function | Description | +|:---------|:------------| +| `isNumber(v)` | Returns true if v is a number | +| `isString(v)` | Returns true if v is a string | +| `isArray(v)` | Returns true if v is an array | +| `isObject(v)` | Returns true if v is an object | +| `isBoolean(v)` | Returns true if v is a boolean | +| `isNull(v)` | Returns true if v is null | +| `isUndefined(v)` | Returns true if v is undefined | + +## Constants + +| Constant | Value | +|:---------|:------| +| `PI` | 3.14159... | +| `E` | 2.71828... | +| `true` | Boolean true | +| `false` | Boolean false | + +## Array Literals + +``` +[1, 2, 3] +["a", "b", "c"] +[1, "mixed", true, [nested]] +``` + +## Object Literals + +``` +{a: 1, b: 2} +{name: "John", age: 30} +{nested: {x: 1, y: 2}} +``` + +## Property Access + +``` +user.name // Object property +user.address.city // Nested property +items[0] // Array index +items[0].name // Combined +``` + +## Arrow Functions + +``` +x => x * 2 // Single parameter +(x, y) => x + y // Multiple parameters +(acc, x) => acc + x // For fold/reduce +``` + +## Variables (if enabled) + +``` +x = 5; x * 2 // Assignment, then use +fn = x => x * 2; fn(3) // Function assignment +``` diff --git a/docs/syntax.md b/docs/syntax.md index a4bd022d..9541a6be 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -1,9 +1,14 @@ # Expression Syntax -The parser accepts a pretty basic grammar. It's similar to normal JavaScript expressions, but is more math-oriented. For example, the `^` operator is exponentiation, not xor. +> **Audience:** Users writing expressions in applications powered by expr-eval. +> **For developers:** See [Parser Configuration](parser.md) to learn how to enable/disable operators. + +The expression language is similar to JavaScript but more math-oriented. For example, the `^` operator is exponentiation, not xor. ## Operator Precedence +Operators are listed from highest to lowest precedence: + | Operator | Associativity | Description | |:------------------------ |:------------- |:----------- | | (...) | None | Grouping | @@ -21,15 +26,7 @@ The parser accepts a pretty basic grammar. It's similar to normal JavaScript exp | = | Right | Variable assignment | | ; | Left | Expression separator | -```js -const parser = new Parser({ - operators: { - 'in': true, - 'assignment': true - } -}); -// Now parser supports 'x in array' and 'y = 2*x' expressions -``` +> **Note:** Some operators like `in`, `=`, and `;` may be disabled by your application. ## Concatenation Operator @@ -46,23 +43,19 @@ The `|` (pipe) operator concatenates arrays or strings: When both operands are arrays, the `|` operator returns a new array containing all elements from both arrays: -```js -const parser = new Parser(); - -parser.evaluate('[1, 2] | [3, 4]'); // [1, 2, 3, 4] -parser.evaluate('[1] | [2] | [3]'); // [1, 2, 3] -parser.evaluate('["a", "b"] | ["c", "d"]'); // ["a", "b", "c", "d"] +``` +[1, 2] | [3, 4] → [1, 2, 3, 4] +[1] | [2] | [3] → [1, 2, 3] +["a", "b"] | ["c", "d"] → ["a", "b", "c", "d"] ``` ### String Concatenation When both operands are strings, the `|` operator returns a new string combining both: -```js -const parser = new Parser(); - -parser.evaluate('"hello" | " " | "world"'); // "hello world" -parser.evaluate('"a" | "b" | "c"'); // "abc" +``` +"hello" | " " | "world" → "hello world" +"a" | "b" | "c" → "abc" ``` > **Note:** Mixing types (e.g., an array with a string) will return `undefined`. @@ -76,7 +69,7 @@ The unary `+` and `-` operators are an exception, and always have their normal p | Operator | Description | |:-------- |:----------- | | -x | Negation | -| +x | Unary plus. This converts it's operand to a number, but has no other effect. | +| +x | Unary plus. This converts its operand to a number, but has no other effect. | | x! | Factorial (x * (x-1) * (x-2) * … * 2 * 1). gamma(x + 1) for non-integers. | | abs x | Absolute value (magnitude) of x | | acos x | Arc cosine of x (in radians) | @@ -151,6 +144,7 @@ Besides the "operator" functions, there are several pre-defined functions. You c |:------------- |:----------- | | if(c, a, b) | Function form of c ? a : b. Note: This always evaluates both `a` and `b`, regardless of whether `c` is `true` or not. Use `c ? a : b` instead if there are side effects, or if evaluating the branches could be expensive. | | coalesce(a, b, ...) | Returns the first non-null and non-empty string value from the arguments. Numbers and booleans (including 0 and false) are considered valid values. | +| json(value) | Converts a value to a JSON string representation. | ### Type Checking Functions @@ -232,68 +226,62 @@ The parser includes comprehensive string manipulation capabilities. ### String Function Examples -```js -const parser = new Parser(); - +``` // String inspection -parser.evaluate('length("hello")'); // 5 -parser.evaluate('isEmpty("")'); // true -parser.evaluate('contains("hello world", "world")'); // true -parser.evaluate('startsWith("hello", "he")'); // true -parser.evaluate('endsWith("hello", "lo")'); // true -parser.evaluate('searchCount("hello hello", "hello")'); // 2 +length("hello") → 5 +isEmpty("") → true +contains("hello world", "world") → true +startsWith("hello", "he") → true +endsWith("hello", "lo") → true +searchCount("hello hello", "hello") → 2 // String transformation -parser.evaluate('trim(" hello ")'); // "hello" -parser.evaluate('trim("**hello**", "*")'); // "hello" -parser.evaluate('toUpper("hello")'); // "HELLO" -parser.evaluate('toLower("HELLO")'); // "hello" -parser.evaluate('toTitle("hello world")'); // "Hello World" -parser.evaluate('repeat("ha", 3)'); // "hahaha" -parser.evaluate('reverse("hello")'); // "olleh" +trim(" hello ") → "hello" +trim("**hello**", "*") → "hello" +toUpper("hello") → "HELLO" +toLower("HELLO") → "hello" +toTitle("hello world") → "Hello World" +repeat("ha", 3) → "hahaha" +reverse("hello") → "olleh" // String extraction -parser.evaluate('left("hello", 3)'); // "hel" -parser.evaluate('right("hello", 3)'); // "llo" -parser.evaluate('split("a,b,c", ",")'); // ["a", "b", "c"] +left("hello", 3) → "hel" +right("hello", 3) → "llo" +split("a,b,c", ",") → ["a", "b", "c"] -// String manipulation -parser.evaluate('replace("hello hello", "hello", "hi")'); // "hi hi" -parser.evaluate('replaceFirst("hello hello", "hello", "hi")'); // "hi hello" +// String replacement +replace("hello hello", "hello", "hi") → "hi hi" +replaceFirst("hello hello", "hello", "hi") → "hi hello" // Natural sorting -parser.evaluate('naturalSort(["file10", "file2", "file1"])'); // ["file1", "file2", "file10"] +naturalSort(["file10", "file2", "file1"]) → ["file1", "file2", "file10"] // Type conversion -parser.evaluate('toNumber("123")'); // 123 -parser.evaluate('toBoolean("true")'); // true -parser.evaluate('toBoolean("yes")'); // true -parser.evaluate('toBoolean("0")'); // false +toNumber("123") → 123 +toBoolean("true") → true +toBoolean("yes") → true +toBoolean("0") → false // Padding -parser.evaluate('padLeft("5", 3)'); // " 5" -parser.evaluate('padLeft("5", 3, "0")'); // "005" -parser.evaluate('padRight("5", 3)'); // "5 " -parser.evaluate('padRight("5", 3, "0")'); // "500" -parser.evaluate('padBoth("hi", 6)'); // " hi " -parser.evaluate('padBoth("hi", 6, "-")'); // "--hi--" +padLeft("5", 3) → " 5" +padLeft("5", 3, "0") → "005" +padRight("5", 3) → "5 " +padBoth("hi", 6) → " hi " +padBoth("hi", 6, "-") → "--hi--" // Slicing -parser.evaluate('slice("hello world", 0, 5)'); // "hello" -parser.evaluate('slice("hello world", -5)'); // "world" -parser.evaluate('slice([1, 2, 3, 4, 5], -2)'); // [4, 5] +slice("hello world", 0, 5) → "hello" +slice("hello world", -5) → "world" +slice([1, 2, 3, 4, 5], -2) → [4, 5] // Encoding -parser.evaluate('urlEncode("foo=bar&baz")'); // "foo%3Dbar%26baz" -parser.evaluate('base64Encode("hello")'); // "aGVsbG8=" -parser.evaluate('base64Decode("aGVsbG8=")'); // "hello" +urlEncode("foo=bar&baz") → "foo%3Dbar%26baz" +base64Encode("hello") → "aGVsbG8=" +base64Decode("aGVsbG8=") → "hello" // Coalesce -parser.evaluate('coalesce("", null, "found")'); // "found" -parser.evaluate('coalesce(null, 0, 42)'); // 0 - -// Complex string operations -parser.evaluate('toUpper(trim(left(" hello world ", 10)))'); // "HELLO WOR" +coalesce("", null, "found") → "found" +coalesce(null, 0, 42) → 0 ``` > **Note:** All string functions return `undefined` if any of their required arguments are `undefined`, allowing for safe chaining and conditional logic. @@ -311,27 +299,20 @@ The parser includes functions for working with objects. ### Object Function Examples -```js -const parser = new Parser(); - +``` // Merge objects -parser.evaluate('merge({a: 1}, {b: 2})'); // {a: 1, b: 2} -parser.evaluate('merge({a: 1, b: 2}, {b: 3, c: 4})'); // {a: 1, b: 3, c: 4} -parser.evaluate('merge({a: 1}, {b: 2}, {c: 3})'); // {a: 1, b: 2, c: 3} +merge({a: 1}, {b: 2}) → {a: 1, b: 2} +merge({a: 1, b: 2}, {b: 3, c: 4}) → {a: 1, b: 3, c: 4} // Get keys -parser.evaluate('keys({a: 1, b: 2, c: 3})'); // ["a", "b", "c"] +keys({a: 1, b: 2, c: 3}) → ["a", "b", "c"] // Get values -parser.evaluate('values({a: 1, b: 2, c: 3})'); // [1, 2, 3] +values({a: 1, b: 2, c: 3}) → [1, 2, 3] -// Flatten nested objects -parser.evaluate('flatten(obj)', { obj: { foo: { bar: 1 } } }); // {foo_bar: 1} -parser.evaluate('flatten(obj)', { obj: { a: { b: { c: 1 } } } }); // {a_b_c: 1} -parser.evaluate('flatten(obj, ".")', { obj: { foo: { bar: 1 } } }); // {"foo.bar": 1} - -// Mixed nested and flat keys -parser.evaluate('flatten(obj)', { obj: { a: 1, b: { c: 2 } } }); // {a: 1, b_c: 2} +// Flatten nested objects (using a variable `obj`) +flatten(obj) // where obj = {foo: {bar: 1}} → {foo_bar: 1} +flatten(obj, ".") // custom separator → {"foo.bar": 1} ``` > **Note:** All object functions return `undefined` if any of their required arguments are `undefined`, allowing for safe chaining and conditional logic. @@ -350,22 +331,22 @@ You can define functions using the syntax `name(params) = expression`. When it's Examples: -```js +``` square(x) = x*x add(a, b) = a + b factorial(x) = x < 2 ? 1 : x * factorial(x - 1) ``` -These functions can than be used in other functions that require a function argument, such as `map`, `filter` or `fold`: +These functions can then be used in other functions that require a function argument, such as `map`, `filter` or `fold`: -```js +``` name(u) = u.name; map(users, name) add(a, b) = a+b; fold([1, 2, 3], 0, add) ``` You can also define the functions inline: -```js +``` filter([1, 2, 3, 4, 5], isEven(x) = x % 2 == 0) ``` @@ -375,49 +356,49 @@ Arrow functions provide a concise syntax for inline functions, similar to JavaSc **Single parameter (no parentheses required):** -```js -map([1, 2, 3], x => x * 2) // [2, 4, 6] -filter([1, 2, 3, 4], x => x > 2) // [3, 4] -map(users, x => x.name) // Extract property from objects +``` +map([1, 2, 3], x => x * 2) → [2, 4, 6] +filter([1, 2, 3, 4], x => x > 2) → [3, 4] +map(users, x => x.name) → Extract property from objects ``` **Multiple parameters (parentheses required):** -```js -fold([1, 2, 3, 4, 5], 0, (acc, x) => acc + x) // 15 (sum) -fold([1, 2, 3, 4, 5], 1, (acc, x) => acc * x) // 120 (product) -map([10, 20, 30], (val, idx) => val + idx) // [10, 21, 32] -filter([10, 20, 30], (x, i) => i >= 1) // [20, 30] +``` +fold([1, 2, 3, 4, 5], 0, (acc, x) => acc + x) → 15 (sum) +fold([1, 2, 3, 4, 5], 1, (acc, x) => acc * x) → 120 (product) +map([10, 20, 30], (val, idx) => val + idx) → [10, 21, 32] +filter([10, 20, 30], (x, i) => i >= 1) → [20, 30] ``` **Zero parameters:** -```js -(() => 42)() // 42 +``` +(() => 42)() → 42 ``` **Assignment to variable:** Arrow functions can be assigned to variables for reuse: -```js -fn = x => x * 2; map([1, 2, 3], fn) // [2, 4, 6] -double = x => x * 2; triple = x => x * 3; map(map([1, 2], triple), double) // [6, 12] +``` +fn = x => x * 2; map([1, 2, 3], fn) → [2, 4, 6] +double = x => x * 2; triple = x => x * 3; map(map([1, 2], triple), double) → [6, 12] ``` **Nested arrow functions:** -```js -map([[1, 2], [3, 4]], row => map(row, x => x * 2)) // [[2, 4], [6, 8]] +``` +map([[1, 2], [3, 4]], row => map(row, x => x * 2)) → [[2, 4], [6, 8]] ``` **With member access and complex expressions:** -```js -filter(users, x => x.age > 25) // Filter objects by property -map(items, x => x.value * 2 + 1) // Complex transformations -filter(numbers, x => x > 0 and x < 10) // Using logical operators -map([3, 7, 2, 9], x => x > 5 ? "high" : "low") // Using ternary operator +``` +filter(users, x => x.age > 25) → Filter objects by property +map(items, x => x.value * 2 + 1) → Complex transformations +filter(numbers, x => x > 0 and x < 10) → Using logical operators +map([3, 7, 2, 9], x => x > 5 ? "high" : "low") → ["low", "high", "low", "high"] ``` > **Note:** Arrow functions share the same `fndef` operator flag as traditional function definitions. If function definitions are disabled via parser options, arrow functions will also be disabled. @@ -428,45 +409,45 @@ The new array utility functions provide additional ways to work with arrays: **Using reduce (alias for fold):** -```js -reduce([1, 2, 3, 4], 0, (acc, x) => acc + x) // 10 (sum using reduce) -reduce([2, 3, 4], 1, (acc, x) => acc * x) // 24 (product) +``` +reduce([1, 2, 3, 4], 0, (acc, x) => acc + x) → 10 (sum using reduce) +reduce([2, 3, 4], 1, (acc, x) => acc * x) → 24 (product) ``` **Using find:** -```js -find([1, 3, 7, 2, 9], x => x > 5) // 7 (first element > 5) -find([1, 2, 3], x => x < 0) // undefined (not found) -find(users, x => x.age > 18) // First user over 18 +``` +find([1, 3, 7, 2, 9], x => x > 5) → 7 (first element > 5) +find([1, 2, 3], x => x < 0) → undefined (not found) +find(users, x => x.age > 18) → First user over 18 ``` **Using some and every:** -```js -some([1, 5, 15, 3], x => x > 10) // true (at least one > 10) -every([1, 2, 3, 4], x => x > 0) // true (all positive) -every([2, 4, 5, 6], x => x % 2 == 0) // false (not all even) -some([1, 2, 3], x => x < 0) // false (none negative) +``` +some([1, 5, 15, 3], x => x > 10) → true (at least one > 10) +every([1, 2, 3, 4], x => x > 0) → true (all positive) +every([2, 4, 5, 6], x => x % 2 == 0) → false (not all even) +some([1, 2, 3], x => x < 0) → false (none negative) ``` **Using unique/distinct:** -```js -unique([1, 2, 2, 3, 3, 3, 4]) // [1, 2, 3, 4] -distinct(["a", "b", "a", "c", "b"]) // ["a", "b", "c"] -unique([]) // [] +``` +unique([1, 2, 2, 3, 3, 3, 4]) → [1, 2, 3, 4] +distinct(["a", "b", "a", "c", "b"]) → ["a", "b", "c"] +unique([]) → [] ``` **Combining array functions:** -```js -// Filter positive numbers, remove duplicates, then double each -unique(filter([1, -2, 3, 3, -4, 5, 1], x => x > 0)) // [1, 3, 5] -map(unique([1, 2, 2, 3]), x => x * 2) // [2, 4, 6] +``` +// Filter positive numbers, remove duplicates +unique(filter([1, -2, 3, 3, -4, 5, 1], x => x > 0)) → [1, 3, 5] +map(unique([1, 2, 2, 3]), x => x * 2) → [2, 4, 6] // Find first even number greater than 5 -find(filter([3, 7, 8, 9, 10], x => x > 5), x => x % 2 == 0) // 8 +find(filter([3, 7, 8, 9, 10], x => x > 5), x => x % 2 == 0) → 8 ``` ### Examples of Type Checking Functions @@ -475,95 +456,182 @@ Type checking functions are useful for validating data types and conditional log **Basic type checking:** -```js -isArray([1, 2, 3]) // true -isNumber(42) // true -isString("hello") // true -isBoolean(true) // true -isNull(null) // true -isUndefined(undefined) // true -isObject({a: 1}) // true -isFunction(abs) // true +``` +isArray([1, 2, 3]) → true +isNumber(42) → true +isString("hello") → true +isBoolean(true) → true +isNull(null) → true +isUndefined(undefined) → true +isObject({a: 1}) → true +isFunction(abs) → true ``` **Using with conditionals:** -```js -if(isArray(x), count(x), 0) // Get array length or 0 -if(isNumber(x), x * 2, x) // Double if number -if(isString(x), toUpper(x), x) // Uppercase if string +``` +if(isArray(x), count(x), 0) → Get array length or 0 +if(isNumber(x), x * 2, x) → Double if number +if(isString(x), toUpper(x), x) → Uppercase if string ``` **Using with filter:** -```js -filter([1, "a", 2, "b", 3], isNumber) // [1, 2, 3] -filter([1, "a", 2, "b", 3], isString) // ["a", "b"] +``` +filter([1, "a", 2, "b", 3], isNumber) → [1, 2, 3] +filter([1, "a", 2, "b", 3], isString) → ["a", "b"] ``` **Using with some/every:** -```js -some([1, 2, "hello", 3], isString) // true (has at least one string) -every([1, 2, 3, 4], isNumber) // true (all are numbers) -every([1, "a", 3], isNumber) // false (not all numbers) +``` +some([1, 2, "hello", 3], isString) → true (has at least one string) +every([1, 2, 3, 4], isNumber) → true (all are numbers) +every([1, "a", 3], isNumber) → false (not all numbers) ``` **Practical examples:** -```js -// Count how many strings are in an array -count(filter([1, "a", 2, "b", 3], isString)) // 2 +``` +count(filter([1, "a", 2, "b", 3], isString)) → 2 (count strings) +find(["a", "b", 3, "c", 5], isNumber) → 3 (first number) +some(data, x => isNull(x) or isUndefined(x)) → Check for null/undefined +``` + +## Custom Functions -// Get the first number in a mixed array -find(["a", "b", 3, "c", 5], isNumber) // 3 +Your application may provide additional custom functions beyond the built-in ones. Check your application's documentation to see what custom functions are available. -// Check if any value is null or undefined -some(data, x => isNull(x) or isUndefined(x)) // true/false +> **For developers:** You can add custom functions via `parser.functions`. See [Parser Configuration](parser.md#parserfunctions) for details. + +## Constants + +The following constants are available in expressions: + +| Constant | Value | Description | +|:-------- |:----- |:----------- | +| E | 2.718... | Euler's number (base of natural logarithms) | +| PI | 3.141... | The ratio of a circle's circumference to its diameter | +| true | true | Logical true value | +| false | false | Logical false value | +| undefined | undefined | Represents a missing or undefined value | + +**Examples:** + +``` +2 * PI → 6.283185307179586 +E ^ 2 → 7.3890560989306495 +true and false → false +x == undefined → true (if x is not defined) ``` -## Custom JavaScript Functions +> **For developers:** Constants can be customized via `parser.consts`. See [Parser Configuration](parser.md#parserconsts) for details. -If you need additional functions that aren't supported out of the box, you can easily add them in your own code. Instances of the `Parser` class have a property called `functions` that's simply an object with all the functions that are in scope. You can add, replace, or delete any of the properties to customize what's available in the expressions. For example: +## Coalesce Operator -```js -const parser = new Parser(); +The `??` operator returns the right operand when the left operand is null, undefined, Infinity, or NaN: -// Add a new function -parser.functions.customAddFunction = function (arg1, arg2) { - return arg1 + arg2; -}; +``` +x ?? 0 → 0 (if x is undefined or null) +y ?? "default" → y (if y has a value) +10 / 0 ?? -1 → -1 (division by zero gives Infinity) +sqrt(-1) ?? 0 → 0 (sqrt of negative gives NaN) +``` -// Remove the factorial function -delete parser.functions.fac; +This is useful for providing default values: -parser.evaluate('customAddFunction(2, 4) == 6'); // true -//parser.evaluate('fac(3)'); // This will fail +``` +user.nickname ?? user.name ?? "Anonymous" +settings.timeout ?? 5000 ``` -## Constants +## Optional Property Access -The parser also includes a number of pre-defined constants that can be used in expressions. These are shown in the table below: +Property access automatically handles missing properties without errors. If any part of a property chain doesn't exist, the result is `undefined` instead of an error: -| Constant | Description | -|:------------ |:----------- | -| E | The value of `Math.E` from your JavaScript runtime | -| PI | The value of `Math.PI` from your JavaScript runtime | -| true | Logical `true` value | -| false | Logical `false` value | +``` +user.profile.name → "Ada" (if exists) +user.profile.email → undefined (if missing, no error) +user.settings.theme → undefined (if settings is missing) +items[99].value → undefined (if index doesn't exist) +``` + +Combined with the coalesce operator: -Pre-defined constants are stored in `parser.consts`. You can make changes to this property to customise the constants available to your expressions. For example: +``` +user.settings.theme ?? "dark" → "dark" (fallback if missing) +items[0].price ?? 0 → 0 (fallback if missing) +``` -```js -const parser = new Parser(); -parser.consts.R = 1.234; +## CASE Expressions -console.log(parser.parse('A+B/R').toString()); // ((A + B) / 1.234) +SQL-style CASE expressions provide multi-way conditionals. + +### Switch-style CASE + +Compare a value against multiple options: + +``` +case status + when "active" then "✓ Active" + when "pending" then "⏳ Pending" + when "inactive" then "✗ Inactive" + else "Unknown" +end ``` -To disable the pre-defined constants, you can replace or delete `parser.consts`: +### Condition-style CASE -```js -const parser = new Parser(); -parser.consts = {}; +Evaluate multiple conditions (like if/else if/else): + +``` +case + when score >= 90 then "A" + when score >= 80 then "B" + when score >= 70 then "C" + when score >= 60 then "D" + else "F" +end +``` + +**Examples:** + +``` +// Categorize a number +case + when x < 0 then "negative" + when x == 0 then "zero" + else "positive" +end + +// Map status codes +case code + when 200 then "OK" + when 404 then "Not Found" + when 500 then "Server Error" + else "Unknown: " + code +end +``` + +## In and Not In Operators + +Check if a value exists in an array: + +``` +"apple" in ["apple", "banana", "cherry"] → true +"grape" in ["apple", "banana", "cherry"] → false +"grape" not in ["apple", "banana", "cherry"] → true +5 in [1, 2, 3, 4, 5] → true +``` + +> **Note:** The `in` operator may be disabled by your application. + +## JSON Function + +Convert values to JSON strings: + +``` +json([1, 2, 3]) → "[1,2,3]" +json({a: 1, b: 2}) → '{"a":1,"b":2}' +json("hello") → '"hello"' ``` diff --git a/src/language-service/diagnostics.ts b/src/language-service/diagnostics.ts index 88d66170..35ce3f91 100644 --- a/src/language-service/diagnostics.ts +++ b/src/language-service/diagnostics.ts @@ -1,7 +1,7 @@ /** * Diagnostics module for the language service. * Provides function argument count validation and syntax error detection. - * + * * This module leverages the existing parser infrastructure for error detection, * avoiding duplication of tokenization and parsing logic. */ diff --git a/src/language-service/language-service.ts b/src/language-service/language-service.ts index 0b7f4157..59fb1d7d 100644 --- a/src/language-service/language-service.ts +++ b/src/language-service/language-service.ts @@ -336,7 +336,7 @@ export function createLanguageService(options: LanguageServiceOptions | undefine try { const ts = makeTokenStream(parser, text); spans = iterateTokens(ts); - } catch (error) { + } catch { // If tokenization fails, we already have a parse error diagnostic // Return early since we can't do function argument checking without tokens return diagnostics; diff --git a/src/language-service/ls-utils.ts b/src/language-service/ls-utils.ts index bcab3208..4f57db72 100644 --- a/src/language-service/ls-utils.ts +++ b/src/language-service/ls-utils.ts @@ -2,6 +2,21 @@ import { Parser } from '../parsing/parser'; import { TEOF, Token, TokenStream } from '../parsing'; +/** + * Returns a human-readable type name for a value. + * + * @param value - The value to get the type name for + * @returns A string describing the type: 'null', 'array', 'function', 'object', + * 'string', 'number', 'boolean', or 'undefined' + * + * @example + * ```ts + * valueTypeName(null) // 'null' + * valueTypeName([1, 2, 3]) // 'array' + * valueTypeName({ a: 1 }) // 'object' + * valueTypeName('hello') // 'string' + * ``` + */ export function valueTypeName(value: Value): string { const t = typeof value; switch (true) { @@ -18,11 +33,50 @@ export function valueTypeName(value: Value): string { } } +/** + * Checks if a character is valid within a property path. + * + * Valid path characters include alphanumerics, underscore, dollar sign, + * dot (for property access), and square brackets (for array indexing). + * + * @param ch - The character to check + * @returns `true` if the character can appear in a property path + * + * @example + * ```ts + * isPathChar('a') // true + * isPathChar('.') // true + * isPathChar('[') // true + * isPathChar(' ') // false + * ``` + */ export function isPathChar(ch: string): boolean { // Include square brackets to keep array selectors within the detected prefix + // eslint-disable-next-line no-useless-escape return /[A-Za-z0-9_$.\[\]]/.test(ch); } +/** + * Extracts a property path prefix from text at a given position. + * + * Scans backward from the position to find the start of a property path, + * useful for providing completions when the user is typing a variable + * or property access expression. + * + * @param text - The full text to extract from + * @param position - The cursor position (0-based offset) + * @returns An object with `start` (the starting offset of the path) and + * `prefix` (the extracted path string) + * + * @example + * ```ts + * extractPathPrefix('user.name', 9) + * // { start: 0, prefix: 'user.name' } + * + * extractPathPrefix('x + user.na', 11) + * // { start: 4, prefix: 'user.na' } + * ``` + */ export function extractPathPrefix(text: string, position: number): { start: number; prefix: string } { const i = Math.max(0, Math.min(position, text.length)); let start = i; @@ -32,6 +86,27 @@ export function extractPathPrefix(text: string, position: number): { start: numb return { start, prefix: text.slice(start, i) }; } +/** + * Converts a value to a truncated JSON string for display in hover previews. + * + * Limits output to prevent overwhelming the UI with large data structures. + * If the value exceeds the limits, the output is truncated with '...'. + * + * @param value - The value to convert to JSON + * @param maxLines - Maximum number of lines to include (default: 3) + * @param maxWidth - Maximum characters per line (default: 50) + * @returns A truncated JSON string, or '' if conversion fails, + * or '' if the result is empty + * + * @example + * ```ts + * toTruncatedJsonString({ a: 1 }) + * // '{\n "a": 1\n}' + * + * toTruncatedJsonString(veryLargeObject) + * // '{\n "key1": "value1",\n "key2": "value2"...' + * ``` + */ export function toTruncatedJsonString(value: unknown, maxLines = 3, maxWidth = 50): string { let text: string; try { @@ -51,12 +126,59 @@ export function toTruncatedJsonString(value: unknown, maxLines = 3, maxWidth = 5 return exceededMaxLength ? lines.join('\n\n') + '...' : lines.join('\n\n'); } +/** + * Creates a TokenStream for tokenizing expression text. + * + * @param parser - The parser instance to use for tokenization + * @param text - The expression text to tokenize + * @returns A TokenStream that can be iterated to get tokens + * + * @example + * ```ts + * const parser = new Parser(); + * const stream = makeTokenStream(parser, '2 + x'); + * ``` + */ export function makeTokenStream(parser: Parser, text: string): TokenStream { return new TokenStream(parser, text); } -export function iterateTokens(ts: TokenStream, untilPos?: number): { token: Token; start: number; end: number }[] { - const spans: { token: Token; start: number; end: number }[] = []; +/** + * Token span information including the token and its position in the source. + */ +export interface TokenSpan { + /** The token object */ + token: Token; + /** Start offset in the source text (0-based) */ + start: number; + /** End offset in the source text (exclusive) */ + end: number; +} + +/** + * Iterates through all tokens in a TokenStream, collecting position information. + * + * Useful for syntax highlighting and finding the token at a specific position. + * Can optionally stop early when reaching a specified position. + * + * @param ts - The TokenStream to iterate + * @param untilPos - Optional position to stop at (will include the token containing this position) + * @returns An array of token spans with start/end positions + * + * @example + * ```ts + * const parser = new Parser(); + * const stream = makeTokenStream(parser, '2 + x * 3'); + * const spans = iterateTokens(stream); + * // Returns spans for: 2, +, x, *, 3 + * + * // Stop early at position 5 + * const partialSpans = iterateTokens(stream, 5); + * // Returns spans for: 2, +, x + * ``` + */ +export function iterateTokens(ts: TokenStream, untilPos?: number): TokenSpan[] { + const spans: TokenSpan[] = []; while (true) { const t = ts.next(); if (t.type === TEOF) { From 257b0e76bb30312cfc741fffd20bdf0bae5d6e7e Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Fri, 23 Jan 2026 20:44:05 +0100 Subject: [PATCH 2/5] Document more breaking changes --- BREAKING_CHANGES.md | 55 +++++++++++++++++++++++++++++++++++++++++++++ docs/migration.md | 29 +++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index b05bcf49..e2746d3e 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -89,3 +89,58 @@ Attempting to access these properties in variable names or member expressions wi parser.evaluate('x.__proto__', { x: {} }); parser.evaluate('__proto__', { __proto__: {} }); ``` + +## Version 4.0.0 + +### Concatenation Operator Changed from `||` to `|` + +**What Changed**: The `||` operator was repurposed for logical OR (JavaScript-style). A new `|` (pipe) operator was introduced for array and string concatenation. Additionally, the `&&` operator was added for logical AND. + +**Before (original expr-eval 2.x)**: +```typescript +// || was used for concatenation +parser.evaluate('"hello" || " world"'); // "hello world" +parser.evaluate('[1, 2] || [3, 4]'); // [1, 2, 3, 4] +``` + +**After (v4.0.0+)**: +```typescript +// | is now used for concatenation +parser.evaluate('"hello" | " world"'); // "hello world" +parser.evaluate('[1, 2] | [3, 4]'); // [1, 2, 3, 4] + +// || is now logical OR +parser.evaluate('true || false'); // true +parser.evaluate('false || true'); // true + +// && is logical AND (new) +parser.evaluate('true && false'); // false +parser.evaluate('true && true'); // true +``` + +**Migration Guide**: + +1. **Find concatenation usage**: Search your expressions for `||` used with strings or arrays +2. **Replace with pipe**: Change `||` to `|` for concatenation operations +3. **Review logical operations**: If you were using `or` keyword, you can now also use `||` + +### Package Renamed + +The package was renamed from `expr-eval` to `@pro-fa/expr-eval` and ported to TypeScript. + +```bash +# Remove old package +npm uninstall expr-eval + +# Install new package +npm install @pro-fa/expr-eval +``` + +Update imports: +```typescript +// Before +const { Parser } = require('expr-eval'); + +// After +import { Parser } from '@pro-fa/expr-eval'; +``` diff --git a/docs/migration.md b/docs/migration.md index 2848f365..c232eaba 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -118,7 +118,34 @@ Access to these properties is now blocked: - `prototype` - `constructor` -See [BREAKING_CHANGES.md](BREAKING_CHANGES.md) for complete details. +### Version 4.0.0 + +**Concatenation operator changed from `||` to `|`:** + +The `||` operator was repurposed for logical OR (JavaScript-style), and a new `|` operator was introduced for concatenation. + +```js +// BEFORE (original expr-eval) +"hello" || " world" // "hello world" (concatenation) +[1, 2] || [3, 4] // [1, 2, 3, 4] (concatenation) +true || false // Not supported or different behavior + +// AFTER (v4.0.0+) +"hello" | " world" // "hello world" (concatenation with |) +[1, 2] | [3, 4] // [1, 2, 3, 4] (concatenation with |) +true || false // true (logical OR) +true && false // false (logical AND) +``` + +**Migration steps:** + +1. Search your expressions for `||` used for string or array concatenation +2. Replace `||` with `|` for concatenation operations +3. `||` now works as logical OR, and `&&` was added as logical AND + +**Package renamed:** + +The package was renamed from `expr-eval` to `@pro-fa/expr-eval` and ported to TypeScript. ## Package Name Change From e8b37623b7b922609efc616b0dda1fd21d4962a5 Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Fri, 23 Jan 2026 21:19:36 +0100 Subject: [PATCH 3/5] Serve docs with mkdocs --- .gitignore | 2 + README.md | 30 ++++++ docs/breaking-changes.md | 102 ++++++++++++++++++ docs/contributing.md | 223 +++++++++++++++++++++++++++++++++++++++ docs/index.md | 84 +++++++++++++++ docs/migration.md | 4 +- mkdocs.yml | 69 ++++++++++++ 7 files changed, 512 insertions(+), 2 deletions(-) create mode 100644 docs/breaking-changes.md create mode 100644 docs/contributing.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index 6136da29..fe6ee2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ coverage *.d.ts.map .tscache/ +# MkDocs +site/ diff --git a/README.md b/README.md index a42e0053..23f5a167 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,36 @@ npm run bench:memory # Memory usage See [docs/performance.md](docs/performance.md) for detailed performance documentation. +## Serving Documentation Locally + +The documentation can be served locally using [MkDocs](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/). + +### Prerequisites + +Install MkDocs Material (requires Python): + +```bash +pip install mkdocs-material +``` + +### Serve Documentation + +```bash +# Start local documentation server +mkdocs serve +``` + +This will start a local server at `http://127.0.0.1:8000` with live reload. + +### Build Static Site + +```bash +# Build static HTML files +mkdocs build +``` + +The static site will be generated in the `site/` directory. + ## License See [LICENSE.txt](LICENSE.txt) for license information. diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md new file mode 100644 index 00000000..48f808c1 --- /dev/null +++ b/docs/breaking-changes.md @@ -0,0 +1,102 @@ +# Breaking Changes + +This document lists breaking changes between versions of `@pro-fa/expr-eval`. + +> **Audience**: Developers upgrading between versions + +For detailed migration instructions, see the [Migration Guide](migration.md). + +## Version 6.0.0 + +### Null Comparison Behavior + +**What Changed**: Null comparisons now follow JavaScript semantics more closely. Previously, `null == undefined` returned `true`, but other null comparisons may have behaved unexpectedly. + +**Migration**: Review expressions that compare values to `null` or `undefined`. Use the `??` (coalesce) operator for null/undefined fallback values. + +## Version 5.0.0 + +### registerFunction Deprecated + +**What Changed**: `registerFunction()` is deprecated. Use direct assignment to `parser.functions` instead. + +**Before**: +```typescript +parser.registerFunction('double', (x) => x * 2); +``` + +**After**: +```typescript +parser.functions.double = (x) => x * 2; +``` + +### Protected Properties + +Access to the following properties is now blocked to prevent prototype pollution attacks: +- `__proto__` +- `prototype` +- `constructor` + +Attempting to access these properties in variable names or member expressions will throw an `AccessError`. + +**Example**: +```typescript +// These will throw AccessError +parser.evaluate('x.__proto__', { x: {} }); +parser.evaluate('__proto__', { __proto__: {} }); +``` + +## Version 4.0.0 + +### Concatenation Operator Changed from `||` to `|` + +**What Changed**: The `||` operator was repurposed for logical OR (JavaScript-style). A new `|` (pipe) operator was introduced for array and string concatenation. Additionally, the `&&` operator was added for logical AND. + +**Before (original expr-eval 2.x)**: +```typescript +// || was used for concatenation +parser.evaluate('"hello" || " world"'); // "hello world" +parser.evaluate('[1, 2] || [3, 4]'); // [1, 2, 3, 4] +``` + +**After (v4.0.0+)**: +```typescript +// | is now used for concatenation +parser.evaluate('"hello" | " world"'); // "hello world" +parser.evaluate('[1, 2] | [3, 4]'); // [1, 2, 3, 4] + +// || is now logical OR +parser.evaluate('true || false'); // true +parser.evaluate('false || true'); // true + +// && is logical AND (new) +parser.evaluate('true && false'); // false +parser.evaluate('true && true'); // true +``` + +**Migration Guide**: + +1. **Find concatenation usage**: Search your expressions for `||` used with strings or arrays +2. **Replace with pipe**: Change `||` to `|` for concatenation operations +3. **Review logical operations**: If you were using `or` keyword, you can now also use `||` + +### Package Renamed + +The package was renamed from `expr-eval` to `@pro-fa/expr-eval` and ported to TypeScript. + +```bash +# Remove old package +npm uninstall expr-eval + +# Install new package +npm install @pro-fa/expr-eval +``` + +Update imports: +```typescript +// Before +const { Parser } = require('expr-eval'); + +// After +import { Parser } from '@pro-fa/expr-eval'; +``` diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..21801f23 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,223 @@ +# Contributing to expr-eval + +Thank you for your interest in contributing to expr-eval! This guide will help you get started with development. + +> **Audience**: Project contributors + +## Development Setup + +### Prerequisites + +- Node.js 18 or higher +- npm 9 or higher + +### Getting Started + +1. **Clone the repository** + ```bash + git clone https://github.com/pro-fa/expr-eval.git + cd expr-eval + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Run tests to verify setup** + ```bash + npm test + ``` + +## Project Structure + +``` +expr-eval/ +├── src/ # Source code +│ ├── index.ts # Main entry point +│ ├── config/ # Parser configuration +│ ├── core/ # Core evaluation logic +│ ├── errors/ # Error types and handling +│ ├── functions/ # Built-in functions +│ │ ├── array/ # Array functions +│ │ ├── math/ # Math functions +│ │ ├── object/ # Object functions +│ │ ├── string/ # String functions +│ │ └── utility/ # Utility functions +│ ├── language-service/ # IDE integration +│ ├── operators/ # Operator implementations +│ │ ├── binary/ # Binary operators +│ │ └── unary/ # Unary operators +│ ├── parsing/ # Tokenizer and parser +│ ├── types/ # TypeScript type definitions +│ └── validation/ # Expression validation +├── test/ # Test files (mirrors src/ structure) +├── docs/ # Documentation +├── benchmarks/ # Performance benchmarks +└── samples/ # Example applications +``` + +## Development Workflow + +### Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage +``` + +### Linting + +```bash +# Run ESLint +npm run lint +``` + +### Building + +```bash +# Build the library +npm run build +``` + +### Benchmarks + +```bash +# Run all benchmarks +npm run bench + +# Run specific benchmark categories +npm run bench:parsing +npm run bench:evaluation +npm run bench:memory +``` + +## Code Style + +### TypeScript Guidelines + +- Use explicit types for function parameters and return values +- Prefer `interface` over `type` for object shapes +- Use `readonly` for properties that shouldn't be modified +- Document public APIs with TSDoc comments + +### Naming Conventions + +- **Files**: `kebab-case.ts` +- **Classes**: `PascalCase` +- **Functions/Methods**: `camelCase` +- **Constants**: `UPPER_SNAKE_CASE` +- **Interfaces**: `PascalCase` (no `I` prefix) + +### Example + +```typescript +/** + * Evaluates a mathematical expression. + * @param expression - The expression string to evaluate + * @param variables - Variable values for the expression + * @returns The evaluation result + */ +export function evaluate( + expression: string, + variables: Record +): Value { + // Implementation +} +``` + +## Testing Guidelines + +### Test File Organization + +- Test files should mirror the source structure +- Use descriptive test names that explain the expected behavior +- Group related tests with `describe` blocks + +### Example Test + +```typescript +import { describe, it, expect } from 'vitest'; +import { Parser } from '../src'; + +describe('Parser', () => { + describe('evaluate', () => { + it('should evaluate simple arithmetic', () => { + const parser = new Parser(); + expect(parser.evaluate('2 + 3')).toBe(5); + }); + + it('should substitute variables', () => { + const parser = new Parser(); + expect(parser.evaluate('x * 2', { x: 5 })).toBe(10); + }); + }); +}); +``` + +## Pull Request Process + +1. **Create a feature branch** + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** + - Write tests for new functionality + - Update documentation as needed + - Follow the code style guidelines + +3. **Run checks locally** + ```bash + npm run lint + npm test + npm run build + ``` + +4. **Commit with a descriptive message** + ```bash + git commit -m "feat: add support for new operator" + ``` + + Follow [Conventional Commits](https://www.conventionalcommits.org/) format: + - `feat:` - New features + - `fix:` - Bug fixes + - `docs:` - Documentation changes + - `test:` - Test additions or modifications + - `refactor:` - Code refactoring + - `perf:` - Performance improvements + +5. **Push and create a PR** + ```bash + git push origin feature/your-feature-name + ``` + +## Adding New Functions + +1. Create the function in the appropriate `src/functions/` subdirectory +2. Export it from the subdirectory's `index.ts` +3. Register it in the parser's default functions +4. Add tests in the corresponding `test/functions/` file +5. Document the function in `docs/syntax.md` + +## Adding New Operators + +1. Create the operator in `src/operators/binary/` or `src/operators/unary/` +2. Add the operator token to the tokenizer +3. Add parser support for the operator precedence +4. Register it in the parser configuration +5. Add tests and documentation + +## Questions? + +If you have questions about contributing, feel free to: +- Open an issue on GitHub +- Check existing issues and discussions + +Thank you for contributing! diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..66d3ed27 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,84 @@ +# expr-eval + +[![npm](https://img.shields.io/npm/v/@pro-fa/expr-eval.svg?maxAge=3600)](https://www.npmjs.com/package/@pro-fa/expr-eval) + +**A safe mathematical expression evaluator for JavaScript and TypeScript.** + +This is a modern TypeScript port of the expr-eval library, completely rewritten with contemporary build tools and development practices. Originally based on [expr-eval 2.0.2](http://silentmatt.com/javascript-expression-evaluator/), this version has been restructured with a modular architecture, TypeScript support, and comprehensive testing using Vitest. + +## What is expr-eval? + +expr-eval parses and evaluates mathematical expressions. It's a safer and more math-oriented alternative to using JavaScript's `eval` function for mathematical expressions. + +It has built-in support for common math operators and functions. Additionally, you can add your own JavaScript functions. Expressions can be evaluated directly, or compiled into native JavaScript functions. + +## Installation + +```bash +npm install @pro-fa/expr-eval +``` + +## Quick Start + +```typescript +import { Parser } from '@pro-fa/expr-eval'; + +const parser = new Parser(); +const expr = parser.parse('2 * x + 1'); +console.log(expr.evaluate({ x: 3 })); // 7 + +// or evaluate directly +Parser.evaluate('6 * x', { x: 7 }); // 42 +``` + +## Key Features + +- **Mathematical Expressions** - Full support for arithmetic, comparison, and logical operators +- **Built-in Functions** - Trigonometry, logarithms, min/max, array operations, string manipulation +- **Custom Functions** - Add your own JavaScript functions +- **Variable Support** - Evaluate expressions with dynamic variable values +- **Expression Compilation** - Convert expressions to native JavaScript functions +- **TypeScript Support** - Full type definitions included +- **Undefined Support** - Graceful handling of undefined values +- **Coalesce Operator** - `??` operator for null/undefined fallback +- **SQL Case Blocks** - SQL-style CASE/WHEN/THEN/ELSE expressions +- **Object Construction** - Create objects and arrays in expressions +- **Language Service** - IDE integration with completions, hover info, and highlighting + +## Playground + +Try out the expression evaluator and its language server capabilities directly in your browser at the [Playground](https://pro-fa.github.io/expr-eval/). The playground provides an interactive environment with: + +- Live expression evaluation +- Code completions and IntelliSense +- Syntax highlighting +- Hover information for functions and variables + +## Documentation Overview + +### For Expression Writers + +If you're writing expressions in an application powered by expr-eval: + +- [Quick Reference](quick-reference.md) - Cheat sheet of operators, functions, and syntax +- [Expression Syntax](syntax.md) - Complete syntax reference with examples + +### For Developers + +If you're integrating expr-eval into your project: + +- [Parser](parser.md) - Parser configuration, methods, and customization +- [Expression](expression.md) - Expression object methods: evaluate, simplify, variables, toJSFunction +- [Advanced Features](advanced-features.md) - Promises, custom resolution, type conversion, operator customization +- [Language Service](language-service.md) - IDE integration: completions, hover info, diagnostics, Monaco Editor +- [Migration Guide](migration.md) - Upgrading from original expr-eval or previous versions + +### For Contributors + +- [Contributing](contributing.md) - Development setup, code style, and PR guidelines +- [Performance Testing](performance.md) - Benchmarks, profiling, and optimization guidance +- [Breaking Changes](breaking-changes.md) - Version-by-version breaking change documentation + +## License + +See [LICENSE.txt](https://github.com/pro-fa/expr-eval/blob/main/LICENSE.txt) for license information. diff --git a/docs/migration.md b/docs/migration.md index c232eaba..fdd75410 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -189,6 +189,6 @@ import { Parser, Expression, Value, Values } from '@pro-fa/expr-eval'; If you encounter issues during migration: -1. Check the [BREAKING_CHANGES.md](BREAKING_CHANGES.md) for detailed breaking change information -2. Review the [documentation](docs/) for the feature you're using +1. Check the [Breaking Changes](breaking-changes.md) for detailed breaking change information +2. Review the documentation for the feature you're using 3. Open an issue on GitHub with a minimal reproduction case diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..82f3cc90 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,69 @@ +site_name: expr-eval +site_description: A safe mathematical expression evaluator for JavaScript and TypeScript +site_url: https://pro-fa.github.io/expr-eval/ +repo_url: https://github.com/pro-fa/expr-eval +repo_name: pro-fa/expr-eval + +theme: + name: material + palette: + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.instant + - navigation.tracking + - navigation.sections + - navigation.expand + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - admonition + - pymdownx.details + - tables + - toc: + permalink: true + +nav: + - Home: index.md + - For Expression Writers: + - Quick Reference: quick-reference.md + - Expression Syntax: syntax.md + - For Developers: + - Parser: parser.md + - Expression: expression.md + - Advanced Features: advanced-features.md + - Language Service: language-service.md + - Migration Guide: migration.md + - For Contributors: + - Contributing: contributing.md + - Performance Testing: performance.md + - Breaking Changes: breaking-changes.md + - Reference: + - Enhancements Summary: enhancements.md From 0843175b74a17b1ed128c940d2d942a501bbdf1e Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Fri, 23 Jan 2026 21:27:38 +0100 Subject: [PATCH 4/5] Update CONTRIBUTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0b1920a..0298141b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Thank you for your interest in contributing to expr-eval! This document provides ```bash # Clone the repository -git clone https://github.com/user/expr-eval.git +git clone https://github.com/pro-fa/expr-eval.git cd expr-eval # Install dependencies From bbca4b8eebcb8893ee653caffc7ccbc4489adea3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:33:39 +0100 Subject: [PATCH 5/5] Deduplicate TokenSpan interface definition (#38) * Initial plan * Remove duplicate TokenSpan interface Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- src/language-service/diagnostics.ts | 10 +--------- src/language-service/language-service.ts | 6 +++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/language-service/diagnostics.ts b/src/language-service/diagnostics.ts index 35ce3f91..4513b628 100644 --- a/src/language-service/diagnostics.ts +++ b/src/language-service/diagnostics.ts @@ -20,6 +20,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { GetDiagnosticsParams, ArityInfo } from './language-service.types'; import { FunctionDetails } from './language-service.models'; import { ParseError } from '../types/errors'; +import type { TokenSpan } from './ls-utils'; /** * Length of the error highlight range when position is known but token length is not. @@ -27,15 +28,6 @@ import { ParseError } from '../types/errors'; */ const ERROR_HIGHLIGHT_LENGTH = 10; -/** - * Represents a token with its position in the source text. - */ -export interface TokenSpan { - token: Token; - start: number; - end: number; -} - /** * State used while counting function arguments. */ diff --git a/src/language-service/language-service.ts b/src/language-service/language-service.ts index 59fb1d7d..27d150ff 100644 --- a/src/language-service/language-service.ts +++ b/src/language-service/language-service.ts @@ -34,14 +34,14 @@ import { valueTypeName, extractPathPrefix, makeTokenStream, - iterateTokens + iterateTokens, + TokenSpan } from './ls-utils'; import { pathVariableCompletions, tryVariableHoverUsingSpans } from './variable-utils'; import { getDiagnosticsForDocument, createDiagnosticFromParseError, - createDiagnosticFromError, - TokenSpan + createDiagnosticFromError } from './diagnostics'; import { ParseError } from '../types/errors';