Skip to content

Commit 1ca87ba

Browse files
committed
Introduce behavior and structure testing
1 parent 6827986 commit 1ca87ba

File tree

12 files changed

+246
-21
lines changed

12 files changed

+246
-21
lines changed

fluent-syntax/makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ compat.js: $(PACKAGE).js
2222
clean:
2323
@rm -f $(PACKAGE).js compat.js
2424
@echo -e " $(OK) clean"
25+
26+
STRUCTURE_FTL := $(wildcard test/fixtures_structure/*.ftl)
27+
STRUCTURE_AST := $(STRUCTURE_FTL:.ftl=.json)
28+
29+
fixtures: $(STRUCTURE_AST)
30+
31+
.PHONY: $(STRUCTURE_AST)
32+
$(STRUCTURE_AST): test/fixtures_structure/%.json: test/fixtures_structure/%.ftl
33+
@../tools/parse.js -s $< > $@
34+
@echo -e " $(OK) $@"

fluent-syntax/src/ast.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,14 @@ export class Span extends Node {
227227
}
228228

229229
export class Annotation extends Node {
230-
constructor(name, message, pos) {
230+
constructor(code, message) {
231231
super();
232232
this.type = 'Annotation';
233-
this.name = name;
233+
this.code = code;
234234
this.message = message;
235-
this.pos = pos;
235+
}
236+
237+
addSpan(start, end) {
238+
this.span = new Span(start, end);
236239
}
237240
}

fluent-syntax/src/errors.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,51 @@
1-
export class ParseError extends Error {}
1+
export class ParseError extends Error {
2+
constructor(code, ...args) {
3+
super();
4+
this.code = code;
5+
this.message = getErrorMessage(code, args);
6+
}
7+
}
8+
9+
function getErrorMessage(code, args) {
10+
switch (code) {
11+
case 'E0001':
12+
return 'Generic error';
13+
case 'E0002':
14+
return 'Expected an entry start';
15+
case 'E0003': {
16+
const [token] = args;
17+
return `Expected token: "${token}"`;
18+
}
19+
case 'E0004': {
20+
const [range] = args;
21+
return `Expected a character from range: "${range}"`;
22+
}
23+
case 'E0005': {
24+
const [id, list] = args;
25+
const fields = list.join(', ');
26+
return `Expected entry "${id}" to have one of the fields: ${fields}`;
27+
}
28+
case 'E0006': {
29+
const [field] = args;
30+
return `Expected field: "${field}"`;
31+
}
32+
case 'E0007':
33+
return 'Keyword cannot end with a whitespace';
34+
case 'E0008':
35+
return 'Callee has to be a simple identifier';
36+
case 'E0009':
37+
return 'Key has to be a simple identifier';
38+
case 'E0010':
39+
return 'Expected one of the variants to be marked as default (*)';
40+
case 'E0011':
41+
return 'Expected at least one variant after "->"';
42+
case 'E0012':
43+
return 'Tags cannot be added to messages with attributes';
44+
case 'E0013':
45+
return 'Expected variant key';
46+
case 'E0014':
47+
return 'Expected literal';
48+
default:
49+
return code;
50+
}
51+
}

fluent-syntax/src/ftlstream.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class FTLParserStream extends ParserStream {
4343
return true;
4444
}
4545

46-
throw new ParseError(`Expected token "${ch}"`);
46+
throw new ParseError('E0003', ch);
4747
}
4848

4949
takeCharIf(ch) {
@@ -167,7 +167,7 @@ export class FTLParserStream extends ParserStream {
167167
this.next();
168168
return ret;
169169
}
170-
throw new ParseError('Expected char range');
170+
throw new ParseError('E0004', 'a-zA-Z');
171171
}
172172

173173
takeIDChar() {

fluent-syntax/src/parser.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ function getEntryOrJunk(ps) {
4545
throw err;
4646
}
4747

48-
const annot = new AST.Annotation(
49-
'ParseError', err.message, ps.getIndex()
50-
);
48+
const annot = new AST.Annotation(err.code, err.message);
49+
annot.addSpan(ps.getIndex(), ps.getIndex());
5150

5251
ps.skipToNextEntryStart();
5352
const nextEntryStart = ps.getIndex();
@@ -79,7 +78,7 @@ function getEntry(ps) {
7978
if (comment) {
8079
return comment;
8180
}
82-
throw new ParseError('Expected entry');
81+
throw new ParseError('E0002');
8382
}
8483

8584
function getComment(ps) {
@@ -151,13 +150,13 @@ function getMessage(ps, comment) {
151150

152151
if (ps.isPeekNextLineTagStart()) {
153152
if (attrs !== undefined) {
154-
throw new ParseError('Tags cannot be added to messages with attributes');
153+
throw new ParseError('E0012');
155154
}
156155
tags = getTags(ps);
157156
}
158157

159158
if (pattern === undefined && attrs === undefined && tags === undefined) {
160-
throw new ParseError('Missing field');
159+
throw new ParseError('E0005', id, ['value', 'attributes', 'tags']);
161160
}
162161

163162
return new AST.Message(id, pattern, attrs, tags, comment);
@@ -183,7 +182,7 @@ function getAttributes(ps) {
183182
const value = getPattern(ps);
184183

185184
if (value === undefined) {
186-
throw new ParseError('Expected field');
185+
throw new ParseError('E0006', 'value');
187186
}
188187

189188
attrs.push(new AST.Attribute(key, value));
@@ -232,7 +231,7 @@ function getVariantKey(ps) {
232231
const ch = ps.current();
233232

234233
if (!ch) {
235-
throw new ParseError('Expected VariantKey');
234+
throw new ParseError('E0013');
236235
}
237236

238237
const cc = ch.charCodeAt(0);
@@ -271,7 +270,7 @@ function getVariants(ps) {
271270
const value = getPattern(ps);
272271

273272
if (!value) {
274-
throw new ParseError('Expected field');
273+
throw new ParseError('E0006', 'value');
275274
}
276275

277276
variants.push(new AST.Variant(key, value, defaultIndex));
@@ -282,7 +281,7 @@ function getVariants(ps) {
282281
}
283282

284283
if (!hasDefault) {
285-
throw new ParseError('Missing default variant');
284+
throw new ParseError('E0010');
286285
}
287286

288287
return variants;
@@ -314,7 +313,7 @@ function getDigits(ps) {
314313
}
315314

316315
if (num.length === 0) {
317-
throw new ParseError('Expected char range');
316+
throw new ParseError('E0004', '0-9');
318317
}
319318

320319
return num;
@@ -432,7 +431,7 @@ function getExpression(ps) {
432431
const variants = getVariants(ps);
433432

434433
if (variants.length === 0) {
435-
throw new ParseError('Missing variants');
434+
throw new ParseError('E0011');
436435
}
437436

438437
ps.expectChar('\n');
@@ -501,7 +500,7 @@ function getCallArgs(ps) {
501500

502501
if (ps.current() === ':') {
503502
if (exp.type !== 'MessageReference') {
504-
throw new ParseError('Forbidden key');
503+
throw new ParseError('E0009');
505504
}
506505

507506
ps.next();
@@ -533,7 +532,7 @@ function getArgVal(ps) {
533532
} else if (ps.currentIs('"')) {
534533
return getString(ps);
535534
}
536-
throw new ParseError('Expected field');
535+
throw new ParseError('E0006', 'value');
537536
}
538537

539538
function getString(ps) {
@@ -556,7 +555,7 @@ function getLiteral(ps) {
556555
const ch = ps.current();
557556

558557
if (!ch) {
559-
throw new ParseError('Expected literal');
558+
throw new ParseError('E0014');
560559
}
561560

562561
if (ps.isNumberStart()) {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import assert from 'assert';
2+
import { join } from 'path';
3+
import { readdir, readfile } from './util';
4+
5+
import { parse } from '../src/parser';
6+
7+
const sigil = '^\/\/~ ';
8+
const reDirective = new RegExp(`${sigil}(.*)[\n$]`, 'gm');
9+
10+
function toObject(obj, cur) {
11+
const [key, value] = cur.split(' ').map(tok => tok.trim());
12+
switch (key.toLowerCase()) {
13+
case 'error':
14+
case 'warning':
15+
case 'hint':
16+
obj.code = value;
17+
break
18+
case 'pos':
19+
obj.start = parseInt(value, 10);
20+
obj.end = parseInt(value, 10);
21+
break
22+
case 'start':
23+
obj.start = parseInt(value, 10);
24+
break
25+
case 'end':
26+
obj.end = parseInt(value, 10);
27+
break
28+
default:
29+
throw new Error(`Unknown preprocessor directive field: ${key}`);
30+
}
31+
32+
return obj;
33+
}
34+
35+
function parseDirective(dir) {
36+
const fields = dir.split(',').map(field => field.trim());
37+
return fields.reduce(toObject, {});
38+
}
39+
40+
function* directives(source) {
41+
let match;
42+
while ((match = reDirective.exec(source)) !== null) {
43+
yield parseDirective(match[1]);
44+
}
45+
}
46+
47+
function preprocess(source) {
48+
return {
49+
directives: [...directives(source)],
50+
source: source.replace(reDirective, ''),
51+
};
52+
}
53+
54+
function toAnnotations(annots, cur) {
55+
return annots.concat(
56+
cur.annotations.map(
57+
({code, span}) => ({ code, start: span.start, end: span.end })
58+
)
59+
);
60+
}
61+
62+
const fixtures = join(__dirname, 'fixtures_behavior');
63+
64+
readdir(fixtures).then(filenames => {
65+
const ftlnames = filenames.filter(
66+
filename => filename.endsWith('.ftl')
67+
);
68+
69+
suite('Behavior tests', function() {
70+
for (const filename of ftlnames) {
71+
const filepath = join(fixtures, filename);
72+
test(filename, function() {
73+
return readfile(filepath).then(file => {
74+
const { directives, source } = preprocess(file);
75+
const ast = parse(source);
76+
const annotations = ast.body.reduce(toAnnotations, []);
77+
assert.deepEqual(directives, annotations);
78+
});
79+
});
80+
}
81+
});
82+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bar = Bar {
2+
//~ ERROR E0004, pos 11
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo = Foo
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo = Foo
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"type": "Resource",
3+
"body": [
4+
{
5+
"type": "Message",
6+
"span": {
7+
"type": "Span",
8+
"start": 0,
9+
"end": 9
10+
},
11+
"annotations": [],
12+
"id": {
13+
"type": "Identifier",
14+
"name": "foo"
15+
},
16+
"value": {
17+
"type": "Pattern",
18+
"elements": [
19+
{
20+
"type": "TextElement",
21+
"value": "Foo"
22+
}
23+
]
24+
},
25+
"attributes": null,
26+
"tags": null,
27+
"comment": null
28+
}
29+
],
30+
"comment": null
31+
}

0 commit comments

Comments
 (0)