Skip to content

Commit d821d05

Browse files
committed
fix(mf2): Treat duplicate option identifiers as a data model error
1 parent e22e4b7 commit d821d05

File tree

3 files changed

+22
-0
lines changed

3 files changed

+22
-0
lines changed

packages/mf2-messageformat/src/__fixtures/test-messages.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@
330330
"src": "no-equal {|42| :number minimumFractionDigits 2}",
331331
"syntaxError": true
332332
},
333+
{ "src": "bad {:placeholder option=x option=x}", "syntaxError": true },
334+
{ "src": "bad {:placeholder ns:option=x ns:option=y}", "syntaxError": true },
333335
{ "src": "bad {:placeholder option=}", "syntaxError": true },
334336
{ "src": "bad {:placeholder option value}", "syntaxError": true },
335337
{ "src": "bad {:placeholder option:value}", "syntaxError": true },
@@ -341,7 +343,12 @@
341343
{ "src": "bad {:placeholder :option=x}", "syntaxError": true },
342344
{ "src": "bad {:placeholder option::x=y}", "syntaxError": true },
343345
{ "src": "bad {$placeholder option}", "syntaxError": true },
346+
{ "src": "no {placeholder end", "syntaxError": true },
344347
{ "src": "no {$placeholder end", "syntaxError": true },
348+
{ "src": "no {:placeholder end", "syntaxError": true },
349+
{ "src": "no {|placeholder| end", "syntaxError": true },
350+
{ "src": "no {|literal} end", "syntaxError": true },
351+
{ "src": "no {|literal or placeholder end", "syntaxError": true },
345352
{ "src": ".match {} * {{foo}}", "syntaxError": true },
346353
{ "src": ".match {|foo|} {|bar|} ** {{foo}}", "syntaxError": true },
347354
{ "src": ".match * {{foo}}", "syntaxError": true },

packages/mf2-messageformat/src/cst-parser/expression.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ function parseFunctionRefOrMarkup(
127127
pos += ws;
128128
const opt = parseOption(ctx, pos);
129129
if (opt.end === pos) break; // error
130+
if (options.some(prev => sameIdentifier(prev.name, opt.name))) {
131+
ctx.onError(
132+
'duplicate-option',
133+
opt.name[0].start,
134+
opt.name.at(-1)?.end ?? -1
135+
);
136+
}
130137
options.push(opt);
131138
pos = opt.end;
132139
}
@@ -182,6 +189,11 @@ function parseIdentifier(
182189
}
183190
}
184191

192+
const sameIdentifier = (a: CST.Identifier, b: CST.Identifier) =>
193+
a.length === b.length &&
194+
a[0].value === b[0].value &&
195+
a[2]?.value === b[2]?.value;
196+
185197
function parseReservedAnnotation(
186198
ctx: ParseContext,
187199
start: number

packages/mf2-messageformat/src/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class MessageSyntaxError extends MessageError {
1919
| 'bad-input-expression'
2020
| 'bad-selector'
2121
| 'duplicate-declaration'
22+
| 'duplicate-option'
2223
| 'extra-content'
2324
| 'forward-reference'
2425
| 'key-mismatch'
@@ -49,6 +50,7 @@ export class MessageSyntaxError extends MessageError {
4950
message = `Syntax parse error: Missing ${expected} at ${start}`;
5051
break;
5152
case 'duplicate-declaration':
53+
case 'duplicate-option':
5254
case 'forward-reference':
5355
case 'key-mismatch':
5456
case 'missing-fallback':
@@ -73,6 +75,7 @@ export class MissingSyntaxError extends MessageSyntaxError {
7375
export class MessageDataModelError extends MessageSyntaxError {
7476
declare type:
7577
| 'duplicate-declaration'
78+
| 'duplicate-option'
7679
| 'forward-reference'
7780
| 'key-mismatch'
7881
| 'missing-fallback';

0 commit comments

Comments
 (0)