summaryrefslogtreecommitdiff
path: root/android_icu4j/src/main/java/android/icu/message2/MFDataModelValidator.java
blob: aecd281628e70e62df741c6c5f2f1b74f9f585e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/* 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);
    }
}