Skip to content

Commit ed06c32

Browse files
committed
fix(evaluator): validate number tokens, fix power associativity and precedence
1 parent 6dad130 commit ed06c32

3 files changed

Lines changed: 30 additions & 9 deletions

File tree

AUDIT_LOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This log tracks all significant changes, updates, and versions in the PaperCache
99
1. **Version Bump**: Bumped version to 0.5.8 across `package.json`, `package-lock.json`, `Cargo.toml`, `Cargo.lock`, and `tauri.conf.json`. Added release note `notes/New Features in v0.5.8.md`.
1010
2. **expr-eval Replacement**: `expr-eval` had a high-severity prototype pollution vulnerability (GHSA-8gw3-rxh4-v6jx, GHSA-jc85-fpwf-qm7x) with no fix available. Replaced with `src/lib/evaluator.ts` — a ~150-line recursive descent parser supporting `+`, `-`, `*`, `/`, `%`, `^`, parentheses, unary operators, and variable scope resolution. Includes 26 unit tests covering arithmetic, precedence, variables, and error cases. Removed `expr-eval` from `package.json`.
1111
3. **TypeScript Strict Mode**: Enabled `"strict": true` in `tsconfig.app.json`. Codebase was already compatible — zero new type errors.
12-
4. **Unused Dependency Removal**: Removed `@tauri-apps/plugin-fs`, `@tauri-apps/plugin-shell` (not in Cargo.toml), and `@emnapi/core`, `@emnapi/runtime` (not imported anywhere).
12+
4. **Unused Dependency Removal**: Removed `@tauri-apps/plugin-fs` and `@tauri-apps/plugin-shell` from `package.json` — these npm packages were not imported anywhere in the JS/TS codebase and had no corresponding Rust plugin in `Cargo.toml`. Note: `@emnapi/core` and `@emnapi/runtime` were initially removed but restored because they are required in the lockfile as transitive WASM fallback dependencies for Linux/Windows CI runners.
1313
5. **API Type Safety**: Changed `onEvent` from `(payload: any) => void` to generic `<T>(name, callback: (payload: T) => void)`, removing the eslint-disable comment.
1414
6. **Coverage Thresholds**: Added minimum coverage guardrails to `vite.config.ts` (statements 65%, branches 50%, functions 55%, lines 65%).
1515

src/lib/evaluator.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,16 @@ describe('evaluate', () => {
105105
it('performs power before multiplication', () => {
106106
expect(evaluate('2 * 3 ^ 2')).toBe(18)
107107
})
108+
109+
it('power is right-associative (2^3^2 = 2^(3^2) = 512)', () => {
110+
expect(evaluate('2 ^ 3 ^ 2')).toBe(512)
111+
})
112+
113+
it('unary minus binds looser than power (-2^2 = -(2^2) = -4)', () => {
114+
expect(evaluate('-2 ^ 2')).toBe(-4)
115+
})
116+
117+
it('rejects malformed number with multiple dots', () => {
118+
expect(() => evaluate('1..2')).toThrow(ParseError)
119+
})
108120
})

src/lib/evaluator.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ function tokenize(input: string): Token[] {
3636
}
3737
if (ch >= '0' && ch <= '9') {
3838
let num = ''
39+
let dotCount = 0
3940
while (i < input.length && ((input[i] >= '0' && input[i] <= '9') || input[i] === '.')) {
41+
if (input[i] === '.') dotCount++
4042
num += input[i]
4143
i++
4244
}
45+
if (dotCount > 1) {
46+
throw new ParseError(`Invalid number: '${num}'`)
47+
}
4348
tokens.push({ type: NUMBER, value: num })
4449
continue
4550
}
@@ -152,13 +157,7 @@ class Parser {
152157
}
153158

154159
factor(): number {
155-
let left = this.unary()
156-
while (this.peek().type === OPERATOR && this.peek().value === '^') {
157-
this.consume()
158-
const right = this.unary()
159-
left = Math.pow(left, right)
160-
}
161-
return left
160+
return this.unary()
162161
}
163162

164163
unary(): number {
@@ -167,7 +166,17 @@ class Parser {
167166
const right = this.unary()
168167
return op === '-' ? -right : right
169168
}
170-
return this.primary()
169+
return this.power()
170+
}
171+
172+
power(): number {
173+
const left = this.primary()
174+
if (this.peek().type === OPERATOR && this.peek().value === '^') {
175+
this.consume()
176+
const right = this.power()
177+
return Math.pow(left, right)
178+
}
179+
return left
171180
}
172181

173182
primary(): number {

0 commit comments

Comments
 (0)