Skip to content

Commit 1a30802

Browse files
authored
Merge pull request #10 from stasm/test-fixtures
Structure and behavior testing
2 parents c51965d + 98360cd commit 1a30802

File tree

66 files changed

+650
-56
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+650
-56
lines changed

fluent/syntax/ast.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ def to_json(value):
1414

1515
def from_json(value):
1616
if isinstance(value, dict):
17-
cls = getattr(sys.modules[__name__], value["type"])
17+
cls = getattr(sys.modules[__name__], value['type'])
1818
args = {
1919
k: from_json(v)
20-
for k, v in value.items() if k != "type"
20+
for k, v in value.items()
21+
if k != 'type'
22+
if k != 'span'
2123
}
22-
return cls(**args)
24+
node = cls(**args)
25+
26+
# Spans need to be added via add_span, not __init__.
27+
if 'span' in value:
28+
span = value['span']
29+
# Message and section comments don't have their own spans.
30+
if span is not None:
31+
node.add_span(span['start'], span['end'])
32+
33+
return node
2334
if isinstance(value, list):
2435
return list(map(from_json, value))
2536
else:
@@ -219,8 +230,11 @@ def __init__(self, start, end):
219230

220231

221232
class Annotation(Node):
222-
def __init__(self, name, message, pos):
233+
def __init__(self, code, args=None, message=None):
223234
super(Annotation, self).__init__()
224-
self.name = name
235+
self.code = code
236+
self.args = args or []
225237
self.message = message
226-
self.pos = pos
238+
239+
def add_span(self, start, end):
240+
self.span = Span(start, end)

fluent/syntax/errors.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,43 @@
1+
from __future__ import unicode_literals
2+
3+
14
class ParseError(Exception):
2-
def __init__(self, message):
3-
self.message = message
5+
def __init__(self, code, *args):
6+
self.code = code
7+
self.args = args
8+
self.message = get_error_message(code, args)
9+
10+
11+
def get_error_message(code, args):
12+
if code == 'E00001':
13+
return 'Generic error'
14+
if code == 'E0002':
15+
return 'Expected an entry start'
16+
if code == 'E0003':
17+
return 'Expected token: "{}"'.format(args[0])
18+
if code == 'E0004':
19+
return 'Expected a character from range: "{}"'.format(args[0])
20+
if code == 'E0005':
21+
msg = 'Expected entry "{}" to have a value, attributes or tags'
22+
return msg.format(args[0])
23+
if code == 'E0006':
24+
return 'Expected field: "{}"'.format(args[0])
25+
if code == 'E0007':
26+
return 'Keyword cannot end with a whitespace'
27+
if code == 'E0008':
28+
return 'Callee has to be a simple identifier'
29+
if code == 'E0009':
30+
return 'Key has to be a simple identifier'
31+
if code == 'E0010':
32+
return 'Expected one of the variants to be marked as default (*)'
33+
if code == 'E0011':
34+
return 'Expected at least one variant after "->"'
35+
if code == 'E0012':
36+
return 'Tags cannot be added to messages with attributes'
37+
if code == 'E0013':
38+
return 'Expected variant key'
39+
if code == 'E0014':
40+
return 'Expected literal'
41+
if code == 'E0015':
42+
return 'Only one variant can be marked as default (*)'
43+
return code

fluent/syntax/ftlstream.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ def expect_char(self, ch):
3232
self.next()
3333
return True
3434

35-
raise ParseError('Expected token "{}"'.format(ch))
35+
if ch == '\n':
36+
# Unicode Character 'SYMBOL FOR NEWLINE' (U+2424)
37+
raise ParseError('E0003', '\u2424')
38+
39+
raise ParseError('E0003', ch)
3640

3741
def take_char_if(self, ch):
3842
if self.ch == ch:
@@ -42,12 +46,15 @@ def take_char_if(self, ch):
4246

4347
def take_char(self, f):
4448
ch = self.ch
45-
if f(ch):
49+
if ch is not None and f(ch):
4650
self.next()
4751
return ch
4852
return None
4953

5054
def is_id_start(self):
55+
if self.ch is None:
56+
return False
57+
5158
cc = ord(self.ch)
5259

5360
return (cc >= 97 and cc <= 122) or \
@@ -62,7 +69,7 @@ def is_number_start(self):
6269
def is_peek_next_line_indented(self):
6370
if not self.current_peek_is('\n'):
6471
return False
65-
72+
6673
self.peek()
6774

6875
if self.current_peek_is(' '):
@@ -75,10 +82,17 @@ def is_peek_next_line_indented(self):
7582
def is_peek_next_line_variant_start(self):
7683
if not self.current_peek_is('\n'):
7784
return False
78-
85+
7986
self.peek()
8087

88+
ptr = self.get_peek_index()
89+
8190
self.peek_line_ws()
91+
92+
if (self.get_peek_index() - ptr == 0):
93+
self.reset_peek()
94+
return False
95+
8296
if self.current_peek_is('*'):
8397
self.peek()
8498

@@ -92,27 +106,64 @@ def is_peek_next_line_variant_start(self):
92106
def is_peek_next_line_attribute_start(self):
93107
if not self.current_peek_is('\n'):
94108
return False
95-
109+
96110
self.peek()
97111

112+
ptr = self.get_peek_index()
113+
98114
self.peek_line_ws()
99115

116+
if (self.get_peek_index() - ptr == 0):
117+
self.reset_peek()
118+
return False
119+
100120
if self.current_peek_is('.'):
101121
self.reset_peek()
102122
return True
103123

104124
self.reset_peek()
105125
return False
106126

127+
def is_peek_next_line_pattern(self):
128+
if not self.current_peek_is('\n'):
129+
return False
130+
131+
self.peek()
132+
133+
ptr = self.get_peek_index()
134+
135+
self.peek_line_ws()
136+
137+
if (self.get_peek_index() - ptr == 0):
138+
self.reset_peek()
139+
return False
140+
141+
if (self.current_peek_is('}') or
142+
self.current_peek_is('.') or
143+
self.current_peek_is('#') or
144+
self.current_peek_is('[') or
145+
self.current_peek_is('*')):
146+
self.reset_peek()
147+
return False
148+
149+
self.reset_peek()
150+
return True
151+
107152
def is_peek_next_line_tag_start(self):
108153

109154
if not self.current_peek_is('\n'):
110155
return False
111156

112157
self.peek()
113158

159+
ptr = self.get_peek_index()
160+
114161
self.peek_line_ws()
115162

163+
if (self.get_peek_index() - ptr == 0):
164+
self.reset_peek()
165+
return False
166+
116167
if self.current_peek_is('#'):
117168
self.reset_peek()
118169
return True
@@ -136,15 +187,15 @@ def take_id_start(self):
136187
self.next()
137188
return ret
138189

139-
raise ParseError('Expected char range')
190+
raise ParseError('E0004', 'a-zA-Z')
140191

141192
def take_id_char(self):
142193
def closure(ch):
143194
cc = ord(ch)
144-
return (cc >= 97 and cc <= 122) or \
145-
(cc >= 65 and cc <= 90) or \
146-
(cc >= 48 and cc <= 57) or \
147-
cc == 95 or cc == 45
195+
return ((cc >= 97 and cc <= 122) or
196+
(cc >= 65 and cc <= 90) or
197+
(cc >= 48 and cc <= 57) or
198+
cc == 95 or cc == 45)
148199
return self.take_char(closure)
149200

150201
def take_symb_char(self):

fluent/syntax/parser.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def get_entry_or_junk(ps):
3939
entry.add_span(entry_start_pos, ps.get_index())
4040
return entry
4141
except ParseError as err:
42-
annot = ast.Annotation("ParseError", err.message, ps.get_index())
42+
annot = ast.Annotation(err.code, err.args, err.message)
43+
annot.add_span(ps.get_index(), ps.get_index())
4344

4445
ps.skip_to_next_entry_start()
4546
next_entry_start = ps.get_index()
@@ -66,7 +67,8 @@ def get_entry(ps):
6667

6768
if comment:
6869
return comment
69-
raise ParseError('Expected entry')
70+
71+
raise ParseError('E0002')
7072

7173
def get_comment(ps):
7274
ps.expect_char('/')
@@ -134,12 +136,11 @@ def get_message(ps, comment):
134136

135137
if ps.is_peek_next_line_tag_start():
136138
if attrs is not None:
137-
raise ParseError(
138-
'Tags cannot be added to messages with attributes')
139+
raise ParseError('E0012')
139140
tags = get_tags(ps)
140141

141142
if pattern is None and attrs is None and tags is None:
142-
raise ParseError('Missing field')
143+
raise ParseError('E0005', id.name)
143144

144145
return ast.Message(id, pattern, attrs, tags, comment)
145146

@@ -161,7 +162,7 @@ def get_attributes(ps):
161162
value = get_pattern(ps)
162163

163164
if value is None:
164-
raise ParseError('Expected field')
165+
raise ParseError('E0006', 'value')
165166

166167
attrs.append(ast.Attribute(key, value))
167168

@@ -202,7 +203,7 @@ def get_variant_key(ps):
202203
ch = ps.current()
203204

204205
if ch is None:
205-
raise ParseError('Expected variant key')
206+
raise ParseError('E0013')
206207

207208
if ps.is_number_start():
208209
return get_number(ps)
@@ -220,6 +221,8 @@ def get_variants(ps):
220221
ps.skip_line_ws()
221222

222223
if ps.current_is('*'):
224+
if has_default:
225+
raise ParseError('E0015')
223226
ps.next()
224227
default_index = True
225228
has_default = True
@@ -235,15 +238,15 @@ def get_variants(ps):
235238
value = get_pattern(ps)
236239

237240
if value is None:
238-
raise ParseError('Expected field')
241+
raise ParseError('E0006', 'value')
239242

240243
variants.append(ast.Variant(key, value, default_index))
241244

242245
if not ps.is_peek_next_line_variant_start():
243246
break
244247

245248
if not has_default:
246-
raise ParseError('Missing default variant')
249+
raise ParseError('E0010')
247250

248251
return variants
249252

@@ -270,7 +273,7 @@ def get_digits(ps):
270273
ch = ps.take_digit()
271274

272275
if len(num) == 0:
273-
raise ParseError('Expected char range')
276+
raise ParseError('E0004', '0-9')
274277

275278
return num
276279

@@ -301,19 +304,16 @@ def get_pattern(ps):
301304
if first_line and len(buffer) != 0:
302305
break
303306

304-
ps.peek()
305-
306-
if not ps.current_peek_is(' '):
307-
ps.reset_peek()
307+
if not ps.is_peek_next_line_pattern():
308308
break
309309

310-
ps.peek_line_ws()
311-
ps.skip_to_peek()
312-
313-
first_line = False
310+
ps.next()
311+
ps.skip_line_ws()
314312

315-
if len(buffer) != 0:
313+
if not first_line:
316314
buffer += ch
315+
316+
first_line = False
317317
continue
318318
elif ch == '\\':
319319
ch2 = ps.peek()
@@ -374,7 +374,7 @@ def get_expression(ps):
374374
variants = get_variants(ps)
375375

376376
if len(variants) == 0:
377-
raise ParseError('Missing variables')
377+
raise ParseError('E0011')
378378

379379
ps.expect_char('\n')
380380
ps.expect_char(' ')
@@ -429,7 +429,7 @@ def get_call_args(ps):
429429

430430
if ps.current_is(':'):
431431
if not isinstance(exp, ast.MessageReference):
432-
raise ParseError('Forbidden key')
432+
raise ParseError('E0009')
433433

434434
ps.next()
435435
ps.skip_line_ws()
@@ -456,7 +456,7 @@ def get_arg_val(ps):
456456
return get_number(ps)
457457
elif ps.current_is('"'):
458458
return get_string(ps)
459-
raise ParseError('Expected field')
459+
raise ParseError('E0006', 'value')
460460

461461
def get_string(ps):
462462
val = ''
@@ -476,7 +476,7 @@ def get_literal(ps):
476476
ch = ps.current()
477477

478478
if ch is None:
479-
raise ParseError('Expected literal')
479+
raise ParseError('E0014')
480480

481481
if ps.is_number_start():
482482
return get_number(ps)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
key = { foo.23 }
2+
3+
//~ ERROR E0004, pos 12, args "a-zA-Z"
4+
5+
key = { foo. }
6+
7+
//~ ERROR E0004, pos 31, args "a-zA-Z"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
foo = Value
2+
.attr = Value 2
3+
//~ ERROR E0002, pos 12
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
key = Value
2+
.label =

0 commit comments

Comments
 (0)