Skip to content

Commit aecf7a9

Browse files
committed
Python: Support unary plus in match patterns
Constructs such as ```python match x: case +5: pass ``` are now allowed in Python 3.15 (just as `case -5` would have previously). To support this new syntax, we need to extend the tree-sitter based parser slightly.
1 parent a409950 commit aecf7a9

8 files changed

Lines changed: 276 additions & 3 deletions

File tree

‎python/extractor/tests/parser/match_new.expected‎

Lines changed: 207 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Module: [1, 0] - [43, 0]
1+
Module: [1, 0] - [61, 0]
22
body: [
33
Match: [1, 0] - [3, 19]
44
subject:
@@ -379,4 +379,210 @@ Module: [1, 0] - [43, 0]
379379
Pass: [42, 8] - [42, 12]
380380
]
381381
]
382+
Match: [44, 0] - [60, 12]
383+
subject:
384+
Name: [44, 6] - [44, 11]
385+
variable: Variable('value', None)
386+
ctx: Load
387+
cases: [
388+
Case: [45, 4] - [46, 12]
389+
pattern:
390+
MatchLiteralPattern: [45, 9] - [45, 11]
391+
literal:
392+
UnaryOp: [45, 10] - [45, 11]
393+
op: UAdd
394+
operand:
395+
Num: [45, 10] - [45, 11]
396+
n: 1
397+
text: '1'
398+
guard: None
399+
body: [
400+
Pass: [46, 8] - [46, 12]
401+
]
402+
Case: [47, 4] - [48, 12]
403+
pattern:
404+
MatchLiteralPattern: [47, 9] - [47, 13]
405+
literal:
406+
UnaryOp: [47, 10] - [47, 13]
407+
op: UAdd
408+
operand:
409+
Num: [47, 10] - [47, 13]
410+
n: 1.5
411+
text: '1.5'
412+
guard: None
413+
body: [
414+
Pass: [48, 8] - [48, 12]
415+
]
416+
Case: [49, 4] - [50, 12]
417+
pattern:
418+
MatchLiteralPattern: [49, 9] - [49, 12]
419+
literal:
420+
UnaryOp: [49, 10] - [49, 12]
421+
op: UAdd
422+
operand:
423+
Num: [49, 10] - [49, 12]
424+
n: 2j
425+
text: '2j'
426+
guard: None
427+
body: [
428+
Pass: [50, 8] - [50, 12]
429+
]
430+
Case: [51, 4] - [52, 12]
431+
pattern:
432+
MatchLiteralPattern: [51, 9] - [51, 14]
433+
literal:
434+
BinOp: [51, 9] - [51, 14]
435+
left:
436+
UnaryOp: [51, 10] - [51, 11]
437+
op: UAdd
438+
operand:
439+
Num: [51, 10] - [51, 11]
440+
n: 1
441+
text: '1'
442+
op: Add
443+
right:
444+
Num: [51, 12] - [51, 14]
445+
n: 2j
446+
text: '2j'
447+
guard: None
448+
body: [
449+
Pass: [52, 8] - [52, 12]
450+
]
451+
Case: [53, 4] - [54, 12]
452+
pattern:
453+
MatchLiteralPattern: [53, 9] - [53, 14]
454+
literal:
455+
BinOp: [53, 9] - [53, 14]
456+
left:
457+
UnaryOp: [53, 10] - [53, 11]
458+
op: UAdd
459+
operand:
460+
Num: [53, 10] - [53, 11]
461+
n: 1
462+
text: '1'
463+
op: Sub
464+
right:
465+
Num: [53, 12] - [53, 14]
466+
n: 2j
467+
text: '2j'
468+
guard: None
469+
body: [
470+
Pass: [54, 8] - [54, 12]
471+
]
472+
Case: [55, 4] - [56, 12]
473+
pattern:
474+
MatchOrPattern: [55, 9] - [55, 30]
475+
patterns: [
476+
MatchLiteralPattern: [55, 9] - [55, 14]
477+
literal:
478+
UnaryOp: [55, 10] - [55, 14]
479+
op: UAdd
480+
operand:
481+
Num: [55, 10] - [55, 14]
482+
n: 16
483+
text: '0x10'
484+
MatchLiteralPattern: [55, 17] - [55, 22]
485+
literal:
486+
UnaryOp: [55, 18] - [55, 22]
487+
op: UAdd
488+
operand:
489+
Num: [55, 18] - [55, 22]
490+
n: 8
491+
text: '0o10'
492+
MatchLiteralPattern: [55, 25] - [55, 30]
493+
literal:
494+
UnaryOp: [55, 26] - [55, 30]
495+
op: UAdd
496+
operand:
497+
Num: [55, 26] - [55, 30]
498+
n: 2
499+
text: '0b10'
500+
]
501+
guard: None
502+
body: [
503+
Pass: [56, 8] - [56, 12]
504+
]
505+
Case: [57, 4] - [58, 12]
506+
pattern:
507+
MatchSequencePattern: [57, 9] - [57, 19]
508+
patterns: [
509+
MatchLiteralPattern: [57, 10] - [57, 12]
510+
literal:
511+
UnaryOp: [57, 11] - [57, 12]
512+
op: UAdd
513+
operand:
514+
Num: [57, 11] - [57, 12]
515+
n: 0
516+
text: '0'
517+
MatchLiteralPattern: [57, 14] - [57, 18]
518+
literal:
519+
UnaryOp: [57, 15] - [57, 18]
520+
op: USub
521+
operand:
522+
Num: [57, 15] - [57, 18]
523+
n: 0.0
524+
text: '0.0'
525+
]
526+
guard: None
527+
body: [
528+
Pass: [58, 8] - [58, 12]
529+
]
530+
Case: [59, 4] - [60, 12]
531+
pattern:
532+
MatchMappingPattern: [59, 9] - [59, 33]
533+
mappings: [
534+
MatchKeyValuePattern: [59, 10] - [59, 15]
535+
key:
536+
MatchLiteralPattern: [59, 10] - [59, 12]
537+
literal:
538+
UnaryOp: [59, 11] - [59, 12]
539+
op: UAdd
540+
operand:
541+
Num: [59, 11] - [59, 12]
542+
n: 1
543+
text: '1'
544+
value:
545+
MatchCapturePattern: [59, 14] - [59, 15]
546+
variable:
547+
Name: [59, 14] - [59, 15]
548+
variable: Variable('x', None)
549+
ctx: Store
550+
MatchKeyValuePattern: [59, 17] - [59, 24]
551+
key:
552+
MatchLiteralPattern: [59, 17] - [59, 21]
553+
literal:
554+
UnaryOp: [59, 18] - [59, 21]
555+
op: UAdd
556+
operand:
557+
Num: [59, 18] - [59, 21]
558+
n: 1.5
559+
text: '1.5'
560+
value:
561+
MatchCapturePattern: [59, 23] - [59, 24]
562+
variable:
563+
Name: [59, 23] - [59, 24]
564+
variable: Variable('y', None)
565+
ctx: Store
566+
MatchKeyValuePattern: [59, 26] - [59, 32]
567+
key:
568+
MatchLiteralPattern: [59, 26] - [59, 29]
569+
literal:
570+
UnaryOp: [59, 27] - [59, 29]
571+
op: UAdd
572+
operand:
573+
Num: [59, 27] - [59, 29]
574+
n: 2j
575+
text: '2j'
576+
value:
577+
MatchCapturePattern: [59, 31] - [59, 32]
578+
variable:
579+
Name: [59, 31] - [59, 32]
580+
variable: Variable('z', None)
581+
ctx: Store
582+
]
583+
guard: None
584+
body: [
585+
Pass: [60, 8] - [60, 12]
586+
]
587+
]
382588
]

‎python/extractor/tests/parser/match_new.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,21 @@ def soft_keywords():
4040
match z:
4141
case w:
4242
pass
43+
44+
match value:
45+
case +1:
46+
pass
47+
case +1.5:
48+
pass
49+
case +2j:
50+
pass
51+
case +1+2j:
52+
pass
53+
case +1-2j:
54+
pass
55+
case +0x10 | +0o10 | +0b10:
56+
pass
57+
case [+0, -0.0]:
58+
pass
59+
case {+1: x, +1.5: y, +2j: z}:
60+
pass

‎python/extractor/tsg-python/python.tsg‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2827,7 +2827,11 @@
28272827
set left_node = (ast-node @left "UnaryOp")
28282828
attr (left_node) _start_location = (location-start @prefix_op)
28292829
attr (left_node) operand = @left.node
2830-
attr (left_node) op = "usub"
2830+
if (eq (source-text @prefix_op) "+") {
2831+
attr (left_node) op = "uadd"
2832+
} else {
2833+
attr (left_node) op = "usub"
2834+
}
28312835
} else {
28322836
set left_node = @left.node
28332837
}

‎python/extractor/tsg-python/tsp/grammar.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ module.exports = grammar({
426426

427427
match_literal_pattern: $ => choice(
428428
seq(
429-
optional(field('prefix_operator', '-')),
429+
optional(field('prefix_operator', choice('-', '+'))),
430430
field('real', choice($.integer, $.float)),
431431
optional(seq(
432432
field('operator', choice('+', '-')),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The Python extractor now supports unary plus in `match` literal patterns, introduced in Python 3.15.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
| test.py:2:11:2:11 | UnaryExpr | UAdd |
2+
| test.py:4:11:4:13 | UnaryExpr | UAdd |
3+
| test.py:6:11:6:12 | UnaryExpr | UAdd |
4+
| test.py:8:11:8:11 | UnaryExpr | UAdd |
5+
| test.py:10:11:10:11 | UnaryExpr | UAdd |
6+
| test.py:12:11:12:14 | UnaryExpr | UAdd |
7+
| test.py:12:19:12:22 | UnaryExpr | UAdd |
8+
| test.py:12:27:12:30 | UnaryExpr | UAdd |
9+
| test.py:14:12:14:12 | UnaryExpr | UAdd |
10+
| test.py:14:16:14:18 | UnaryExpr | USub |
11+
| test.py:16:12:16:12 | UnaryExpr | UAdd |
12+
| test.py:16:19:16:21 | UnaryExpr | UAdd |
13+
| test.py:16:28:16:29 | UnaryExpr | UAdd |
14+
| test.py:18:11:18:11 | UnaryExpr | USub |
15+
| test.py:20:11:20:11 | UnaryExpr | USub |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
match value:
2+
case +1:
3+
pass
4+
case +1.5:
5+
pass
6+
case +2j:
7+
pass
8+
case +1+2j:
9+
pass
10+
case +1-2j:
11+
pass
12+
case +0x10 | +0o10 | +0b10:
13+
pass
14+
case [+0, -0.0]:
15+
pass
16+
case {+1: x, +1.5: y, +2j: z}:
17+
pass
18+
case -1+2j:
19+
pass
20+
case -1-2j:
21+
pass
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import python
2+
3+
from UnaryExpr expr
4+
where expr.getLocation().getFile().getShortName() = "test.py"
5+
select expr, expr.getOp().toString()

0 commit comments

Comments
 (0)