Skip to content

Commit b44172b

Browse files
authored
fix: #3501 parse % as unary only when not followed by a term (#3505)
1 parent 5655d71 commit b44172b

File tree

2 files changed

+26
-22
lines changed

2 files changed

+26
-22
lines changed

src/expression/parse.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,16 +1050,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
10501050
// explicit operators
10511051
name = state.token
10521052
fn = operators[name]
1053-
10541053
getTokenSkipNewline(state)
1055-
1056-
if (name === '%' && state.tokenType === TOKENTYPE.DELIMITER && state.token !== '(') {
1057-
// This % cannot be interpreted as a modulus, and it wasn't handled by parseUnaryPostfix
1058-
throw createSyntaxError(state, 'Unexpected operator %')
1059-
} else {
1060-
last = parseImplicitMultiplication(state)
1061-
node = new OperatorNode(name, fn, [node, last])
1062-
}
1054+
last = parseImplicitMultiplication(state)
1055+
node = new OperatorNode(name, fn, [node, last])
10631056
} else {
10641057
break
10651058
}
@@ -1167,13 +1160,19 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
11671160
if (state.token === '%') {
11681161
const previousState = Object.assign({}, state)
11691162
getTokenSkipNewline(state)
1170-
1171-
if (state.tokenType === TOKENTYPE.DELIMITER && state.token !== '(') {
1172-
// This is unary postfix %, then treat that as /100
1173-
node = new OperatorNode('/', 'divide', [node, new ConstantNode(100)], false, true)
1174-
} else {
1175-
// Not a match, so rewind
1163+
// We need to decide if this is a unary percentage % or binary modulo %
1164+
// So we attempt to parse a unary expression at this point.
1165+
// If it fails, then the only possibility is that this is a unary percentage.
1166+
// If it succeeds, then we presume that this must be binary modulo, since the
1167+
// only things that parseUnary can handle are _higher_ precedence than unary %.
1168+
try {
1169+
parseUnary(state)
1170+
// Not sure if we could somehow use the result of that parseUnary? Without
1171+
// further analysis/testing, safer just to discard and let the parse proceed
11761172
Object.assign(state, previousState)
1173+
} catch {
1174+
// Not seeing a term at this point, so was a unary %
1175+
node = new OperatorNode('/', 'divide', [node, new ConstantNode(100)], false, true)
11771176
}
11781177
}
11791178

test/unit-tests/expression/parse.test.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,35 +1374,40 @@ describe('parse', function () {
13741374
it('should parse % with multiplication', function () {
13751375
approxEqual(parseAndEval('100*50%'), 50)
13761376
approxEqual(parseAndEval('50%*100'), 50)
1377-
assert.throws(function () { parseAndEval('50%(*100)') }, /Value expected/)
1377+
assert.throws(function () { parseAndEval('50%(*100)') }, SyntaxError)
13781378
})
13791379

13801380
it('should parse % with division', function () {
13811381
approxEqual(parseAndEval('100/50%'), 200) // should be treated as 100/(50%)
13821382
approxEqual(parseAndEval('100/50%*2'), 400) // should be treated as (100/(50%))×2
13831383
approxEqual(parseAndEval('50%/100'), 0.005)
13841384
approxEqual(parseAndEval('50%(13)'), 11) // should be treated as 50 % (13)
1385-
assert.throws(function () { parseAndEval('50%(/100)') }, /Value expected/)
1385+
assert.throws(function () { parseAndEval('50%(/100)') }, SyntaxError)
13861386
})
13871387

13881388
it('should parse unary % before division, binary % with division', function () {
13891389
approxEqual(parseAndEval('10/200%%3'), 2) // should be treated as (10/(200%))%3
13901390
})
13911391

13921392
it('should reject repeated unary percentage operators', function () {
1393-
assert.throws(function () { math.parse('17%%') }, /Unexpected operator %/)
1394-
assert.throws(function () { math.parse('17%%*5') }, /Unexpected operator %/)
1395-
assert.throws(function () { math.parse('10/200%%%3') }, /Unexpected operator %/)
1393+
assert.throws(function () { math.parse('17%%') }, SyntaxError)
1394+
assert.throws(function () { math.parse('17%%*5') }, SyntaxError)
1395+
assert.throws(function () { math.parse('10/200%%%3') }, SyntaxError)
13961396
})
13971397

13981398
it('should parse unary % with addition', function () {
13991399
approxEqual(parseAndEval('100+3%'), 103)
1400-
approxEqual(parseAndEval('3%+100'), 100.03)
1400+
assert.strictEqual(parseAndEval('3%+100'), 3) // treat as 3 mod 100
14011401
})
14021402

14031403
it('should parse unary % with subtraction', function () {
14041404
approxEqual(parseAndEval('100-3%'), 97)
1405-
approxEqual(parseAndEval('3%-100'), -99.97)
1405+
assert.strictEqual(parseAndEval('3%-100'), -97) // treat as 3 mod -100
1406+
})
1407+
1408+
it('should parse binary % with bitwise negation', function () {
1409+
assert.strictEqual(parseAndEval('11%~1'), -1) // equivalent to 11 mod -2
1410+
assert.strictEqual(parseAndEval('11%~-3'), 1) // equivalent to 11 mod 2
14061411
})
14071412

14081413
it('should parse operator mod', function () {

0 commit comments

Comments
 (0)