Skip to content

Commit 0cbef93

Browse files
committed
feat: expand SyntaxError variants
Adds further copy for a number of variants of a SyntaxError Includes these in the demo page Ensures demo coverage through a new test Fixes incorrect patch for trailing comma error
1 parent 7db6c04 commit 0cbef93

5 files changed

Lines changed: 370 additions & 3 deletions

File tree

copydecks/en/copydeck.json

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"NameError": {
2020
"variants": [
2121
{
22+
"if": {
23+
"not_message": ["is not defined"]
24+
},
2225
"title": "This variable doesn't exist yet",
2326
"summary": "Your code uses the variable \"{{name}}\", but it hasn't been created yet. Check {{loc}}. If you meant to print the text <i>{{name}}</i>, put it in double quotes.",
2427
"why": "Without speech marks Python treats <i>{{name}}</i> as a variable, and this variable does not exist yet.",
@@ -61,7 +64,7 @@
6164
{
6265
"if": {
6366
"match_code": ["^(\\s*)(if|for|while|def|class|elif|else|try|except|with)\\b"],
64-
"not_code": [":\\s*$"]
67+
"not_code": [":\\s*$", ",\\s*$"]
6568
},
6669
"title": "Missing colon (:) at the end",
6770
"summary": "There is a colon (:) missing at the end of a line. Check {{loc}}: {{codeLine}}",
@@ -70,6 +73,119 @@
7073
"Add a colon (:) at the end of that line."
7174
]
7275
},
76+
{
77+
"if": {
78+
"match_code": ["^(\\s*)(if|for|while|def|class|elif|else|try|except|with)\\b.*?,\\s*$"]
79+
},
80+
"title": "Comma used where a colon is needed",
81+
"summary": "This line ends with a comma, but this kind of Python line should end with a colon (:). Check {{loc}}: {{codeLine}}",
82+
"why": "Lines that start a block (like if or for) need a colon, not a comma.",
83+
"steps": [
84+
"Replace the comma with a colon (:)."
85+
]
86+
},
87+
{
88+
"if": {
89+
"match_code": ["^(\\s*)(if|for|while|def|class|elif|else|try|except|with)\\b.*::\\s*$"]
90+
},
91+
"title": "Too many colons at the end",
92+
"summary": "This line ends with more than one colon. Check {{loc}}: {{codeLine}}",
93+
"why": "A block-starting line needs exactly one colon at the end.",
94+
"steps": [
95+
"Leave only one colon (:) at the end of the line."
96+
]
97+
},
98+
{
99+
"if": {
100+
"match_message": ["was never closed"]
101+
},
102+
"title": "A bracket or parenthesis was not closed",
103+
"summary": "One of your brackets, braces, or parentheses is open but never closes.",
104+
"why": "Python needs every opening symbol like (, [ or { to have a matching closing symbol.",
105+
"steps": [
106+
"Find the opening symbol that has no matching closing symbol.",
107+
"Add the missing closing symbol.",
108+
"Check nearby lines as the opening symbol may be earlier."
109+
]
110+
},
111+
{
112+
"if": {
113+
"match_message": ["does not match", "closing parenthesis", "closing bracket", "closing brace"]
114+
},
115+
"title": "Closing bracket does not match the opening one",
116+
"summary": "A closing symbol was used, but it does not match the opening symbol.",
117+
"why": "Each opening symbol must be closed by the same kind: () [] or {}.",
118+
"steps": [
119+
"Find the opening symbol first.",
120+
"Change the closing symbol so it matches.",
121+
"Check that symbols are in the correct order."
122+
]
123+
},
124+
{
125+
"if": {
126+
"match_message": ["unterminated string literal", "EOL while scanning string literal"]
127+
},
128+
"title": "A string was started but not finished",
129+
"summary": "A string opens with a quote mark but does not close.",
130+
"why": "Text in Python must have matching opening and closing quotes.",
131+
"steps": [
132+
"Find the string that starts but does not end.",
133+
"Add the missing quote mark.",
134+
"Use matching quote marks around the text."
135+
]
136+
},
137+
{
138+
"if": {
139+
"match_message": ["perhaps you forgot a comma"]
140+
},
141+
"title": "A comma is missing between items",
142+
"summary": "Python expected a comma between values, but did not find one.",
143+
"why": "Lists, dictionaries, tuples, and function arguments need commas to separate each item.",
144+
"steps": [
145+
"Check where two values are next to each other.",
146+
"Add a comma between each separate item.",
147+
"Run the code again to check for the next issue."
148+
]
149+
},
150+
{
151+
"if": {
152+
"match_message": ["cannot assign to"],
153+
"match_code": ["^(\\s*)(if|elif|while)\\b.*="],
154+
"not_code": ["==", "<=", ">=", "!="]
155+
},
156+
"title": "Assignment used instead of comparison",
157+
"summary": "This condition uses =, but conditions must compare values. Check {{loc}}: {{codeLine}}",
158+
"why": "In conditions, = sets a value, but == compares values.",
159+
"steps": [
160+
"Use == to compare values in if, elif, or while conditions.",
161+
"Keep = for setting a variable value outside comparisons."
162+
]
163+
},
164+
{
165+
"if": {
166+
"match_message": ["unexpected EOF while parsing", "incomplete input"]
167+
},
168+
"title": "Python reached the end before the code was complete",
169+
"summary": "Python got to the end of the file, but something was still unfinished.",
170+
"why": "A line or block was started but not completed, such as a bracket, quote, or block body.",
171+
"steps": [
172+
"Check the final lines of your code first.",
173+
"Look for missing closing brackets, quotes, or colons.",
174+
"Make sure every started block has code inside it."
175+
]
176+
},
177+
{
178+
"if": {
179+
"match_code": ["\\+\\+|--"]
180+
},
181+
"title": "This operator is not valid in Python",
182+
"summary": "This line contains ++ or --, which Python does not use.",
183+
"why": "Python does not have increment/decrement operators like some other languages.",
184+
"steps": [
185+
"Use += 1 to increase a value by one.",
186+
"Use -= 1 to decrease a value by one."
187+
]
188+
},
73189
{
74190
"title": "The line is incomplete or mismatched",
75191
"summary": "Something is missing or extra - like a colon, bracket, or quote.",

docs/demo-examples.js

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
export const examples = [
22
{
3-
title: "NameError - Undefined Variable",
3+
title: "NameError - Variable Not Created Yet",
44
runtime: "skulpt",
5+
expectedVariantId: "NameError/variants/0",
6+
code: `def show_total():
7+
print(total)
8+
9+
show_total()`,
10+
trace: `Traceback (most recent call last):
11+
File "main.py", line 4, in <module>
12+
File "main.py", line 2, in show_total
13+
NameError: free variable 'total' referenced before assignment in enclosing scope`
14+
},
15+
{
16+
title: "NameError - Variable Not Defined Here",
17+
runtime: "skulpt",
18+
expectedVariantId: "NameError/variants/1",
519
code: `print("Hello")
620
print(kittens)`,
721
trace: `Traceback (most recent call last):
@@ -11,27 +25,168 @@ NameError: name 'kittens' is not defined`
1125
{
1226
title: "SyntaxError - Missing Colon",
1327
runtime: "skulpt",
28+
expectedVariantId: "SyntaxError/variants/0",
1429
code: `for i in range(3)
1530
print(i)`,
1631
trace: `Traceback (most recent call last):
1732
File "main.py", line 1
1833
for i in range(3)
1934
^
2035
SyntaxError: invalid syntax`
36+
},
37+
{
38+
title: "SyntaxError - Comma Instead Of Colon",
39+
runtime: "pyodide",
40+
expectedVariantId: "SyntaxError/variants/1",
41+
code: `if score > 10,
42+
print("Great")`,
43+
trace: `Traceback (most recent call last):
44+
File "main.py", line 1
45+
if score > 10,
46+
^
47+
SyntaxError: invalid syntax`
48+
},
49+
{
50+
title: "SyntaxError - Too Many Colons",
51+
runtime: "pyodide",
52+
expectedVariantId: "SyntaxError/variants/2",
53+
code: `if score > 10::
54+
print("Great")`,
55+
trace: `Traceback (most recent call last):
56+
File "main.py", line 1
57+
if score > 10::
58+
^
59+
SyntaxError: invalid syntax`
60+
},
61+
{
62+
title: "SyntaxError - Unterminated String",
63+
runtime: "pyodide",
64+
expectedVariantId: "SyntaxError/variants/5",
65+
code: `print("Hello`,
66+
trace: `Traceback (most recent call last):
67+
File "main.py", line 1
68+
print("Hello
69+
^
70+
SyntaxError: unterminated string literal (detected at line 1)`
71+
},
72+
{
73+
title: "SyntaxError - Missing Comma",
74+
runtime: "pyodide",
75+
expectedVariantId: "SyntaxError/variants/6",
76+
code: `numbers = [1 2, 3]`,
77+
trace: `Traceback (most recent call last):
78+
File "main.py", line 1
79+
numbers = [1 2, 3]
80+
^
81+
SyntaxError: invalid syntax. Perhaps you forgot a comma?`
82+
},
83+
{
84+
title: "SyntaxError - Bracket Not Closed",
85+
runtime: "pyodide",
86+
expectedVariantId: "SyntaxError/variants/3",
87+
code: `total = (1 + 2`,
88+
trace: `Traceback (most recent call last):
89+
File "main.py", line 1
90+
total = (1 + 2
91+
^
92+
SyntaxError: '(' was never closed`
93+
},
94+
{
95+
title: "SyntaxError - Closing Bracket Mismatch",
96+
runtime: "pyodide",
97+
expectedVariantId: "SyntaxError/variants/4",
98+
code: `values = [1, 2, 3)
99+
print(values)`,
100+
trace: `Traceback (most recent call last):
101+
File "main.py", line 1
102+
values = [1, 2, 3)
103+
^
104+
SyntaxError: closing parenthesis ')' does not match opening bracket '['`
105+
},
106+
{
107+
title: "SyntaxError - Assignment In Condition",
108+
runtime: "pyodide",
109+
expectedVariantId: "SyntaxError/variants/7",
110+
code: `if score = 10:
111+
print("Great")`,
112+
trace: `Traceback (most recent call last):
113+
File "main.py", line 1
114+
if score = 10:
115+
^
116+
SyntaxError: cannot assign to name 'score'`
117+
},
118+
{
119+
title: "SyntaxError - Incomplete Input",
120+
runtime: "pyodide",
121+
expectedVariantId: "SyntaxError/variants/8",
122+
code: `value =`,
123+
trace: `Traceback (most recent call last):
124+
File "main.py", line 3
125+
value =
126+
^
127+
SyntaxError: incomplete input`
128+
},
129+
{
130+
title: "SyntaxError - Invalid Operator",
131+
runtime: "pyodide",
132+
expectedVariantId: "SyntaxError/variants/9",
133+
code: `count = 0
134+
count++`,
135+
trace: `Traceback (most recent call last):
136+
File "main.py", line 2
137+
count++
138+
^
139+
SyntaxError: invalid syntax`
140+
},
141+
{
142+
title: "SyntaxError - Generic Mismatch",
143+
runtime: "pyodide",
144+
expectedVariantId: "SyntaxError/variants/10",
145+
code: `result = 1 +* 2`,
146+
trace: `Traceback (most recent call last):
147+
File "main.py", line 1
148+
result = 1 +* 2
149+
^
150+
SyntaxError: invalid syntax`
151+
},
152+
{
153+
title: "IndentationError - Unexpected Indent",
154+
runtime: "pyodide",
155+
expectedVariantId: "IndentationError/variants/0",
156+
code: `print("Start")
157+
print("Oops")`,
158+
trace: `Traceback (most recent call last):
159+
File "main.py", line 2
160+
print("Oops")
161+
^
162+
IndentationError: unexpected indent`
21163
},
22164
{
23165
title: "AttributeError - Using .push() on List",
24166
runtime: "pyodide",
167+
expectedVariantId: "AttributeError/variants/0",
25168
code: `items = []
26169
items.push(3)`,
27170
trace: `Traceback (most recent call last):
28171
File "main.py", line 2, in <module>
29172
items.push(3)
30173
AttributeError: 'list' object has no attribute 'push'`
174+
},
175+
{
176+
title: "AttributeError - Unknown Method",
177+
runtime: "pyodide",
178+
expectedVariantId: "AttributeError/variants/1",
179+
code: `name = "Ada"
180+
name.shrink()`,
181+
trace: `Traceback (most recent call last):
182+
File "main.py", line 2, in <module>
183+
name.shrink()
184+
AttributeError: 'str' object has no attribute 'shrink'`
31185
},
32186
{
33187
title: "TypeError - Adding String and Number",
34188
runtime: "pyodide",
189+
expectedVariantId: "TypeError/variants/0",
35190
code: `age = 10
36191
message = "I am " + age + " years old"`,
37192
trace: `Traceback (most recent call last):
@@ -42,6 +197,7 @@ TypeError: can only concatenate str (not "int") to str`
42197
{
43198
title: "NameError - Variable Used Before Assignment",
44199
runtime: "skulpt",
200+
expectedVariantId: "UnboundLocalError/variants/0",
45201
code: `def calculate():
46202
result = x + 5
47203
x = 10
@@ -57,6 +213,7 @@ UnboundLocalError: local variable 'x' referenced before assignment`
57213
{
58214
title: "IndexError - List Index Out of Range",
59215
runtime: "pyodide",
216+
expectedVariantId: "IndexError/variants/0",
60217
code: `numbers = [1, 2, 3]
61218
print(numbers[5])`,
62219
trace: `Traceback (most recent call last):
@@ -67,6 +224,7 @@ IndexError: list index out of range`
67224
{
68225
title: "KeyError - Dictionary Key Not Found",
69226
runtime: "skulpt",
227+
expectedVariantId: "KeyError/variants/0",
70228
code: `person = {"name": "Alice", "age": 30}
71229
print(person["city"])`,
72230
trace: `Traceback (most recent call last):
@@ -77,11 +235,22 @@ KeyError: 'city'`
77235
{
78236
title: "ZeroDivisionError - Division by Zero",
79237
runtime: "pyodide",
238+
expectedVariantId: "ZeroDivisionError/variants/0",
80239
code: `result = 10 / 0
81240
print(result)`,
82241
trace: `Traceback (most recent call last):
83242
File "main.py", line 1, in <module>
84243
result = 10 / 0
85244
ZeroDivisionError: division by zero`
245+
},
246+
{
247+
title: "Other - ValueError Fallback",
248+
runtime: "pyodide",
249+
expectedVariantId: "Other/variants/0",
250+
code: `int("abc")`,
251+
trace: `Traceback (most recent call last):
252+
File "main.py", line 1, in <module>
253+
int("abc")
254+
ValueError: invalid literal for int() with base 10: 'abc'`
86255
}
87256
];

src/engine.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ const pickVariant = (trace: Trace, code: string | undefined) => {
8686
} else if (trace.type === "NameError" && trace.name) {
8787
patch = `${trace.name} = 0\n${codeLine}`;
8888
} else if (trace.type === "SyntaxError" && /^(if|for|while|def|class|elif|else|try|except|with)\b/i.test(codeLine) && !/:$/.test(codeLine.trim())) {
89-
patch = codeLine.replace(/\s*$/, "") + ":";
89+
const trimmedCodeLine = codeLine.replace(/\s*$/, "");
90+
patch = /,\s*$/.test(trimmedCodeLine)
91+
? trimmedCodeLine.replace(/,\s*$/, ":")
92+
: trimmedCodeLine + ":";
9093
} else if (trace.type === "TypeError" && /\+\s*[A-Za-z_][A-Za-z0-9_]*/.test(codeLine)) {
9194
patch = codeLine.replace(/\+\s*([A-Za-z_][A-Za-z0-9_]*)/, "+ str($1)");
9295
}

0 commit comments

Comments
 (0)