UNPKG

messageformat

Version:

Intl.MessageFormat / Unicode MessageFormat 2 parser, runtime and polyfill

103 lines (102 loc) 3.95 kB
import { MessageDataModelError } from "../errors.js"; import { visit } from "./visit.js"; /** * Ensure that the `msg` data model is _valid_, calling `onError` on errors. * If `onError` is not defined, a {@link MessageDataModelError} will be thrown on error. * * Detects the following errors: * * - `'key-mismatch'`: **Variant Key Mismatch**<br> * The number of keys on a _variant_ does not equal the number of _selectors_. * * - `'missing-fallback'`: **Missing Fallback Variant**<br> * The message does not include a _variant_ with only catch-all keys. * * - `'missing-selector-annotation'`: **Missing Selector Annotation**<br> * A _selector_ does not contains a _variable_ that directly or indirectly * reference a _declaration_ with a _function_. * * - `'duplicate-declaration'`: **Duplicate Declaration**<br> * A _variable_ appears in two _declarations_. * * - `'duplicate-variant'`: **Duplicate Variant**<br> * The same list of _keys_ is used for more than one _variant_. * * @category Message Data Model * @returns The sets of runtime `functions` and `variables` used by the message. */ export function validate(msg, onError = (type, node) => { throw new MessageDataModelError(type, node); }) { let selectorCount = 0; let missingFallback = null; /** Tracks directly & indirectly annotated variables for `missing-selector-annotation` */ const annotated = new Set(); /** Tracks declared variables for `duplicate-declaration` */ const declared = new Set(); const functions = new Set(); const localVars = new Set(); const variables = new Set(); const variants = new Set(); let setArgAsDeclared = true; visit(msg, { declaration(decl) { // Skip all ReservedStatement if (!decl.name) return undefined; if (decl.value.functionRef || (decl.type === 'local' && decl.value.arg?.type === 'variable' && annotated.has(decl.value.arg.name))) { annotated.add(decl.name); } if (decl.type === 'local') localVars.add(decl.name); setArgAsDeclared = decl.type === 'local'; return () => { if (declared.has(decl.name)) onError('duplicate-declaration', decl); else declared.add(decl.name); }; }, expression({ functionRef }) { if (functionRef) functions.add(functionRef.name); }, value(value, context, position) { if (value.type !== 'variable') return; variables.add(value.name); switch (context) { case 'declaration': if (position !== 'arg' || setArgAsDeclared) { declared.add(value.name); } break; case 'selector': selectorCount += 1; missingFallback = value; if (!annotated.has(value.name)) { onError('missing-selector-annotation', value); } } }, variant(variant) { const { keys } = variant; if (keys.length !== selectorCount) onError('key-mismatch', variant); const strKeys = JSON.stringify(keys.map(key => (key.type === 'literal' ? key.value : 0))); if (variants.has(strKeys)) onError('duplicate-variant', variant); else variants.add(strKeys); missingFallback &&= keys.every(key => key.type === '*') ? null : variant; } }); if (missingFallback) onError('missing-fallback', missingFallback); for (const lv of localVars) variables.delete(lv); return { functions, variables }; }