206 lines
8.4 KiB
Java
206 lines
8.4 KiB
Java
![]() |
/* GENERATED SOURCE. DO NOT MODIFY. */
|
||
|
// © 2024 and later: Unicode, Inc. and others.
|
||
|
// License & terms of use: https://www.unicode.org/copyright.html
|
||
|
|
||
|
package android.icu.message2;
|
||
|
|
||
|
import java.util.HashSet;
|
||
|
import java.util.List;
|
||
|
import java.util.Set;
|
||
|
import java.util.StringJoiner;
|
||
|
|
||
|
import android.icu.message2.MFDataModel.Annotation;
|
||
|
import android.icu.message2.MFDataModel.CatchallKey;
|
||
|
import android.icu.message2.MFDataModel.Declaration;
|
||
|
import android.icu.message2.MFDataModel.Expression;
|
||
|
import android.icu.message2.MFDataModel.FunctionAnnotation;
|
||
|
import android.icu.message2.MFDataModel.FunctionExpression;
|
||
|
import android.icu.message2.MFDataModel.InputDeclaration;
|
||
|
import android.icu.message2.MFDataModel.Literal;
|
||
|
import android.icu.message2.MFDataModel.LiteralExpression;
|
||
|
import android.icu.message2.MFDataModel.LiteralOrCatchallKey;
|
||
|
import android.icu.message2.MFDataModel.LiteralOrVariableRef;
|
||
|
import android.icu.message2.MFDataModel.LocalDeclaration;
|
||
|
import android.icu.message2.MFDataModel.Option;
|
||
|
import android.icu.message2.MFDataModel.PatternMessage;
|
||
|
import android.icu.message2.MFDataModel.SelectMessage;
|
||
|
import android.icu.message2.MFDataModel.VariableExpression;
|
||
|
import android.icu.message2.MFDataModel.VariableRef;
|
||
|
import android.icu.message2.MFDataModel.Variant;
|
||
|
|
||
|
// I can merge all this in the MFDataModel class and make it private
|
||
|
class MFDataModelValidator {
|
||
|
private final MFDataModel.Message message;
|
||
|
private final Set<String> declaredVars = new HashSet<>();
|
||
|
|
||
|
MFDataModelValidator(MFDataModel.Message message) {
|
||
|
this.message = message;
|
||
|
}
|
||
|
|
||
|
boolean validate() throws MFParseException {
|
||
|
if (message instanceof PatternMessage) {
|
||
|
validateDeclarations(((PatternMessage) message).declarations);
|
||
|
} else if (message instanceof SelectMessage) {
|
||
|
SelectMessage sm = (SelectMessage) message;
|
||
|
validateDeclarations(sm.declarations);
|
||
|
validateSelectors(sm.selectors);
|
||
|
int selectorCount = sm.selectors.size();
|
||
|
validateVariants(sm.variants, selectorCount);
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private boolean validateVariants(List<Variant> variants, int selectorCount)
|
||
|
throws MFParseException {
|
||
|
if (variants == null || variants.isEmpty()) {
|
||
|
error("Selection messages must have at least one variant");
|
||
|
}
|
||
|
|
||
|
// Look for an entry with all keys = '*'
|
||
|
boolean hasUltimateFallback = false;
|
||
|
Set<String> fakeKeys = new HashSet<>();
|
||
|
for (Variant variant : variants) {
|
||
|
if (variant.keys == null || variant.keys.isEmpty()) {
|
||
|
error("Selection variants must have at least one key");
|
||
|
}
|
||
|
if (variant.keys.size() != selectorCount) {
|
||
|
error("Selection variants must have the same number of variants as the selectors.");
|
||
|
}
|
||
|
int catchAllCount = 0;
|
||
|
StringJoiner fakeKey = new StringJoiner("<<::>>");
|
||
|
for (LiteralOrCatchallKey key : variant.keys) {
|
||
|
if (key instanceof CatchallKey) {
|
||
|
catchAllCount++;
|
||
|
fakeKey.add("*");
|
||
|
} else if (key instanceof Literal) {
|
||
|
fakeKey.add(((Literal) key).value);
|
||
|
}
|
||
|
}
|
||
|
if (fakeKeys.contains(fakeKey.toString())) {
|
||
|
error("Dumplicate combination of keys");
|
||
|
} else {
|
||
|
fakeKeys.add(fakeKey.toString());
|
||
|
}
|
||
|
if (catchAllCount == selectorCount) {
|
||
|
hasUltimateFallback = true;
|
||
|
}
|
||
|
}
|
||
|
if (!hasUltimateFallback) {
|
||
|
error("There must be one variant with all the keys being '*'");
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private boolean validateSelectors(List<Expression> selectors) throws MFParseException {
|
||
|
if (selectors == null || selectors.isEmpty()) {
|
||
|
error("Selection messages must have selectors");
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* .input {$foo :number} .input {$foo} => ERROR
|
||
|
* .input {$foo :number} .local $foo={$bar} => ERROR, local foo overrides an input
|
||
|
* .local $foo={...} .local $foo={...} => ERROR, foo declared twice
|
||
|
* .local $a={$foo} .local $b={$foo} => NOT AN ERROR (foo is used, not declared)
|
||
|
* .local $a={:f opt=$foo} .local $foo={$foo} => ERROR, foo declared after beeing used in opt
|
||
|
*/
|
||
|
private boolean validateDeclarations(List<Declaration> declarations) throws MFParseException {
|
||
|
if (declarations == null || declarations.isEmpty()) {
|
||
|
return true;
|
||
|
}
|
||
|
for (Declaration declaration : declarations) {
|
||
|
if (declaration instanceof LocalDeclaration) {
|
||
|
LocalDeclaration ld = (LocalDeclaration) declaration;
|
||
|
validateExpression(ld.value, false);
|
||
|
addVariableDeclaration(ld.name);
|
||
|
} else if (declaration instanceof InputDeclaration) {
|
||
|
InputDeclaration id = (InputDeclaration) declaration;
|
||
|
validateExpression(id.value, true);
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* One might also consider checking if the same variable is used with more than one type:
|
||
|
* .local $a = {$foo :number}
|
||
|
* .local $b = {$foo :string}
|
||
|
* .local $c = {$foo :datetime}
|
||
|
*
|
||
|
* But this is not necesarily an error.
|
||
|
* If $foo is a number, then it might be formatter as a number, or as date (epoch time),
|
||
|
* or something else.
|
||
|
*
|
||
|
* So it is not safe to complain. Especially with custom functions:
|
||
|
* # get the first name from a `Person` object
|
||
|
* .local $b = {$person :getField fieldName=firstName}
|
||
|
* # get formats a `Person` object
|
||
|
* .local $b = {$person :person}
|
||
|
*/
|
||
|
private void validateExpression(Expression expression, boolean fromInput)
|
||
|
throws MFParseException {
|
||
|
String argName = null;
|
||
|
Annotation annotation = null;
|
||
|
if (expression instanceof Literal) {
|
||
|
// ...{foo}... or ...{|foo|}... or ...{123}...
|
||
|
// does not declare anything
|
||
|
} else if (expression instanceof LiteralExpression) {
|
||
|
LiteralExpression le = (LiteralExpression) expression;
|
||
|
argName = le.arg.value;
|
||
|
annotation = le.annotation;
|
||
|
} else if (expression instanceof VariableExpression) {
|
||
|
VariableExpression ve = (VariableExpression) expression;
|
||
|
// ...{$foo :bar opt1=|str| opt2=$x opt3=$y}...
|
||
|
// .input {$foo :number} => declares `foo`, if already declared is an error
|
||
|
// .local $a={$foo} => declares `a`, but only used `foo`, does not declare it
|
||
|
argName = ve.arg.name;
|
||
|
annotation = ve.annotation;
|
||
|
} else if (expression instanceof FunctionExpression) {
|
||
|
// ...{$foo :bar opt1=|str| opt2=$x opt3=$y}...
|
||
|
FunctionExpression fe = (FunctionExpression) expression;
|
||
|
annotation = fe.annotation;
|
||
|
}
|
||
|
|
||
|
if (annotation instanceof FunctionAnnotation) {
|
||
|
FunctionAnnotation fa = (FunctionAnnotation) annotation;
|
||
|
if (fa.options != null) {
|
||
|
for (Option opt : fa.options.values()) {
|
||
|
LiteralOrVariableRef val = opt.value;
|
||
|
if (val instanceof VariableRef) {
|
||
|
// We had something like {:f option=$val}, it means we's seen `val`
|
||
|
// It is not a declaration, so not an error.
|
||
|
addVariableDeclaration(((VariableRef) val).name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We chech the argument name after options to prevent errors like this:
|
||
|
// .local $foo = {$a :b option=$foo}
|
||
|
if (argName != null) {
|
||
|
// if we come from `.input {$foo :function}` then `varName` is null
|
||
|
// and `argName` is `foo`
|
||
|
if (fromInput) {
|
||
|
addVariableDeclaration(argName);
|
||
|
} else {
|
||
|
// Remember that we've seen it, to complain if there is a declaration later
|
||
|
declaredVars.add(argName);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private boolean addVariableDeclaration(String varName) throws MFParseException {
|
||
|
if (declaredVars.contains(varName)) {
|
||
|
error("Variable '" + varName + "' already declared");
|
||
|
return false;
|
||
|
}
|
||
|
declaredVars.add(varName);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void error(String text) throws MFParseException {
|
||
|
throw new MFParseException(text, -1);
|
||
|
}
|
||
|
}
|