Skip to content

Commit 97047be

Browse files
authored
Merge pull request #396 from stasm/throw-errors
Omitting errors in formatPattern() makes it throw
2 parents 5bea9d1 + 19c724e commit 97047be

8 files changed

+203
-75
lines changed

fluent/src/bundle.js

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {resolveComplexPattern} from "./resolver.js";
22
import FluentResource from "./resource.js";
33
import { FluentNone } from "./types.js";
4+
import Scope from "./scope.js";
45

56
/**
67
* Message bundles are single-language stores of translations. They are
@@ -207,40 +208,32 @@ export default class FluentBundle {
207208
* // [<ReferenceError: Unknown variable: name>]
208209
* }
209210
*
211+
* If `errors` is omitted, the first encountered error will be thrown.
212+
*
210213
* @param {string|Array} pattern
211214
* @param {?Object} args
212215
* @param {?Array} errors
213216
* @returns {string}
214217
*/
215218
formatPattern(pattern, args, errors) {
216-
// Resolve a simple pattern without creating a scope.
219+
// Resolve a simple pattern without creating a scope. No error handling is
220+
// required; by definition simple patterns don't have placeables.
217221
if (typeof pattern === "string") {
218222
return this._transform(pattern);
219223
}
220224

221225
// Resolve a complex pattern.
222-
if (Array.isArray(pattern)) {
223-
let scope = this._createScope(args, errors);
224-
try {
225-
let value = resolveComplexPattern(scope, pattern);
226-
return value.toString(scope);
227-
} catch (err) {
226+
let scope = new Scope(this, errors, args);
227+
try {
228+
let value = resolveComplexPattern(scope, pattern);
229+
return value.toString(scope);
230+
} catch (err) {
231+
if (scope.errors) {
228232
scope.errors.push(err);
229233
return new FluentNone().toString(scope);
230234
}
235+
throw err;
231236
}
232-
233-
throw new TypeError("Invalid Pattern type");
234-
}
235-
236-
_createScope(args, errors = []) {
237-
return {
238-
args, errors,
239-
bundle: this,
240-
dirty: new WeakSet(),
241-
// TermReferences are resolved in a new scope.
242-
insideTermReference: false,
243-
};
244237
}
245238

246239
_memoizeIntlObject(ctor, opts) {

fluent/src/resolver.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function getDefault(scope, variants, star) {
7979
return resolvePattern(scope, variants[star].value);
8080
}
8181

82-
scope.errors.push(new RangeError("No default"));
82+
scope.reportError(new RangeError("No default"));
8383
return new FluentNone();
8484
}
8585

@@ -127,7 +127,7 @@ function resolveExpression(scope, expr) {
127127
function VariableReference(scope, {name}) {
128128
if (!scope.args || !scope.args.hasOwnProperty(name)) {
129129
if (scope.insideTermReference === false) {
130-
scope.errors.push(new ReferenceError(`Unknown variable: ${name}`));
130+
scope.reportError(new ReferenceError(`Unknown variable: ${name}`));
131131
}
132132
return new FluentNone(`$${name}`);
133133
}
@@ -150,7 +150,7 @@ function VariableReference(scope, {name}) {
150150
return new FluentDateTime(arg);
151151
}
152152
default:
153-
scope.errors.push(
153+
scope.reportError(
154154
new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`)
155155
);
156156
return new FluentNone(`$${name}`);
@@ -161,7 +161,7 @@ function VariableReference(scope, {name}) {
161161
function MessageReference(scope, {name, attr}) {
162162
const message = scope.bundle._messages.get(name);
163163
if (!message) {
164-
scope.errors.push(new ReferenceError(`Unknown message: ${name}`));
164+
scope.reportError(new ReferenceError(`Unknown message: ${name}`));
165165
return new FluentNone(name);
166166
}
167167

@@ -170,15 +170,15 @@ function MessageReference(scope, {name, attr}) {
170170
if (attribute) {
171171
return resolvePattern(scope, attribute);
172172
}
173-
scope.errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
173+
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
174174
return new FluentNone(`${name}.${attr}`);
175175
}
176176

177177
if (message.value) {
178178
return resolvePattern(scope, message.value);
179179
}
180180

181-
scope.errors.push(new ReferenceError(`No value: ${name}`));
181+
scope.reportError(new ReferenceError(`No value: ${name}`));
182182
return new FluentNone(name);
183183
}
184184

@@ -187,21 +187,20 @@ function TermReference(scope, {name, attr, args}) {
187187
const id = `-${name}`;
188188
const term = scope.bundle._terms.get(id);
189189
if (!term) {
190-
const err = new ReferenceError(`Unknown term: ${id}`);
191-
scope.errors.push(err);
190+
scope.reportError(new ReferenceError(`Unknown term: ${id}`));
192191
return new FluentNone(id);
193192
}
194193

195-
// Every TermReference has its own args.
196-
const [, keyargs] = getArguments(scope, args);
197-
const local = {...scope, args: keyargs, insideTermReference: true};
194+
// Every TermReference has its own variables.
195+
const [, params] = getArguments(scope, args);
196+
const local = scope.cloneForTermReference(params);
198197

199198
if (attr) {
200199
const attribute = term.attributes[attr];
201200
if (attribute) {
202201
return resolvePattern(local, attribute);
203202
}
204-
scope.errors.push(new ReferenceError(`Unknown attribute: ${attr}`));
203+
scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
205204
return new FluentNone(`${id}.${attr}`);
206205
}
207206

@@ -214,12 +213,12 @@ function FunctionReference(scope, {name, args}) {
214213
// the `FluentBundle` constructor.
215214
const func = scope.bundle._functions[name] || builtins[name];
216215
if (!func) {
217-
scope.errors.push(new ReferenceError(`Unknown function: ${name}()`));
216+
scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
218217
return new FluentNone(`${name}()`);
219218
}
220219

221220
if (typeof func !== "function") {
222-
scope.errors.push(new TypeError(`Function ${name}() is not callable`));
221+
scope.reportError(new TypeError(`Function ${name}() is not callable`));
223222
return new FluentNone(`${name}()`);
224223
}
225224

@@ -252,7 +251,7 @@ function SelectExpression(scope, {selector, variants, star}) {
252251
// Resolve a pattern (a complex string with placeables).
253252
export function resolveComplexPattern(scope, ptn) {
254253
if (scope.dirty.has(ptn)) {
255-
scope.errors.push(new RangeError("Cyclic reference"));
254+
scope.reportError(new RangeError("Cyclic reference"));
256255
return new FluentNone();
257256
}
258257

fluent/src/scope.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export default class Scope {
2+
constructor(
3+
bundle,
4+
errors,
5+
args,
6+
insideTermReference = false,
7+
dirty = new WeakSet()
8+
) {
9+
this.bundle = bundle;
10+
this.errors = errors;
11+
this.args = args;
12+
13+
// Term references require different variable lookup logic.
14+
this.insideTermReference = insideTermReference;
15+
// Keeps track of visited Patterns. Used to detect cyclic references.
16+
this.dirty = dirty;
17+
}
18+
19+
cloneForTermReference(args) {
20+
return new Scope(this.bundle, this.errors, args, true, this.dirty);
21+
}
22+
23+
reportError(error) {
24+
if (!this.errors) {
25+
throw error;
26+
}
27+
this.errors.push(error);
28+
}
29+
}

fluent/test/bomb_test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ suite('Reference bombs', function() {
3535
const val = bundle.formatPattern(msg.value, args, errs);
3636
assert.strictEqual(val, '{???}');
3737
assert.strictEqual(errs.length, 1);
38+
assert.ok(errs[0] instanceof RangeError);
39+
});
40+
41+
test('throws when errors are undefined', function() {
42+
const msg = bundle.getMessage('lolz');
43+
assert.throws(
44+
() => bundle.formatPattern(msg.value),
45+
RangeError,
46+
"Too many characters in placeable"
47+
);
3848
});
3949
});
4050
});

fluent/test/errors_test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use strict";
2+
3+
import assert from "assert";
4+
import ftl from "@fluent/dedent";
5+
6+
import FluentBundle from "../src/bundle";
7+
8+
suite("Errors", function() {
9+
let bundle;
10+
11+
suiteSetup(function() {
12+
bundle = new FluentBundle("en-US", { useIsolating: false });
13+
bundle.addMessages(ftl`
14+
foo = {$one} and {$two}
15+
`);
16+
});
17+
18+
test("Reporting into an array", function() {
19+
let errors = [];
20+
let message = bundle.getMessage("foo");
21+
22+
let val1 = bundle.formatPattern(message.value, {}, errors);
23+
assert.strictEqual(val1, "{$one} and {$two}");
24+
assert.strictEqual(errors.length, 2);
25+
assert.ok(errors[0] instanceof ReferenceError);
26+
assert.ok(errors[1] instanceof ReferenceError);
27+
28+
let val2 = bundle.formatPattern(message.value, {}, errors);
29+
assert.strictEqual(val2, "{$one} and {$two}");
30+
assert.strictEqual(errors.length, 4);
31+
assert.ok(errors[0] instanceof ReferenceError);
32+
assert.ok(errors[1] instanceof ReferenceError);
33+
assert.ok(errors[2] instanceof ReferenceError);
34+
assert.ok(errors[3] instanceof ReferenceError);
35+
});
36+
37+
test("First error is thrown", function() {
38+
let message = bundle.getMessage("foo");
39+
assert.throws(
40+
() => bundle.formatPattern(message.value, {}),
41+
ReferenceError,
42+
"Unknown variable: $one"
43+
);
44+
});
45+
});

0 commit comments

Comments
 (0)