xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/test/CheckPlaceHolders.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.test;
2 
3 import com.google.common.base.Joiner;
4 import com.google.common.base.Splitter;
5 import com.google.common.collect.ImmutableSet;
6 import com.google.common.collect.Multimap;
7 import com.google.common.collect.Sets;
8 import com.ibm.icu.text.UnicodeSet;
9 import com.ibm.icu.util.Output;
10 import java.util.Collection;
11 import java.util.Collections;
12 import java.util.EnumSet;
13 import java.util.HashSet;
14 import java.util.Iterator;
15 import java.util.LinkedHashSet;
16 import java.util.List;
17 import java.util.Map.Entry;
18 import java.util.Set;
19 import java.util.TreeSet;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
23 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Type;
24 import org.unicode.cldr.util.CLDRConfig;
25 import org.unicode.cldr.util.CLDRFile;
26 import org.unicode.cldr.util.LocaleIDParser;
27 import org.unicode.cldr.util.LocaleNames;
28 import org.unicode.cldr.util.Pair;
29 import org.unicode.cldr.util.PatternCache;
30 import org.unicode.cldr.util.XPathParts;
31 import org.unicode.cldr.util.personname.PersonNameFormatter;
32 import org.unicode.cldr.util.personname.PersonNameFormatter.Field;
33 import org.unicode.cldr.util.personname.PersonNameFormatter.FormatParameters;
34 import org.unicode.cldr.util.personname.PersonNameFormatter.ModifiedField;
35 import org.unicode.cldr.util.personname.PersonNameFormatter.Modifier;
36 import org.unicode.cldr.util.personname.PersonNameFormatter.NamePattern;
37 import org.unicode.cldr.util.personname.PersonNameFormatter.Order;
38 import org.unicode.cldr.util.personname.PersonNameFormatter.Usage;
39 
40 public class CheckPlaceHolders extends CheckCLDR {
41 
42     private static final Pattern PLACEHOLDER_PATTERN = PatternCache.get("([0-9]|[1-9][0-9]+)");
43     private static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults();
44     private static final Joiner JOIN_SPACE = Joiner.on(' ');
45 
46     private static final Pattern SKIP_PATH_LIST =
47             Pattern.compile("//ldml/characters/(exemplarCharacters|parseLenient).*");
48 
49     //    private static final LocaleMatchValue LOCALE_MATCH_VALUE = new
50     // LocaleMatchValue(ImmutableSet.of(
51     //        Validity.Status.regular,
52     //        Validity.Status.special,
53     //        Validity.Status.unknown)
54     //        );
55 
56     /** Contains all CLDR locales, plus some special cases */
57     private static final Set<String> CLDR_LOCALES_FOR_NAME_ORDER;
58 
59     static {
60         Set<String> valid = new HashSet<>();
61         valid.addAll(CLDRConfig.getInstance().getCldrFactory().getAvailable());
62         valid.add(LocaleNames.ZXX);
63         valid.add(LocaleNames.UND);
64         CLDR_LOCALES_FOR_NAME_ORDER = ImmutableSet.copyOf(valid);
65     }
66 
67     private static final ImmutableSet<Modifier> SINGLE_CORE = ImmutableSet.of(Modifier.core);
68     private static final ImmutableSet<Modifier> SINGLE_PREFIX = ImmutableSet.of(Modifier.prefix);
69     private static final ImmutableSet<Modifier> CORE_AND_PREFIX =
70             ImmutableSet.of(Modifier.prefix, Modifier.core);
71 
72     private Set<Modifier> allowedModifiers = null;
73 
74     @Override
handleSetCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)75     public CheckCLDR handleSetCldrFileToCheck(
76             CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
77         super.handleSetCldrFileToCheck(cldrFileToCheck, options, possibleErrors);
78         allowedModifiers = Modifier.getAllowedModifiers(cldrFileToCheck.getLocaleID());
79         return this;
80     }
81 
82     @Override
handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)83     public CheckCLDR handleCheck(
84             String path, String fullPath, String value, Options options, List<CheckStatus> result) {
85         if (value == null || path.endsWith("/alias") || SKIP_PATH_LIST.matcher(path).matches()) {
86             return this;
87         }
88         // TODO: more skips here
89         if (!accept(result)) return this;
90 
91         if (path.contains("/personNames")) {
92             XPathParts parts = XPathParts.getFrozenInstance(path);
93             switch (parts.getElement(2)) {
94                 default:
95                     break; // skip to rest of handleCheck
96                 case "initialPattern":
97                     checkInitialPattern(this, path, value, result);
98                     break; // skip to rest of handleCheck
99                 case "nativeSpaceReplacement":
100                 case "foreignSpaceReplacement":
101                     checkForeignSpaceReplacement(this, value, result);
102                     return this;
103                 case "nameOrderLocales":
104                     checkNameOrder(this, path, value, result);
105                     return this;
106                 case "sampleName":
107                     checkSampleNames(this, parts, value, result);
108                     return this;
109                 case "personName":
110                     checkPersonNamePatterns(
111                             this, allowedModifiers, getLocaleID(), path, parts, value, result);
112                     return this;
113             }
114             // done with person names
115             // note: depending on the switch value, may fall through
116         }
117 
118         checkBasicPlaceholders(value, result);
119         checkListPatterns(path, value, result);
120         return this;
121     }
122 
123     /** Verify the that nameOrder items are clean. */
checkNameOrder( CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)124     public static void checkNameOrder(
125             CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) {
126         // ldml/personNames/nameOrderLocales[@order="givenFirst"]
127         final String localeID = checkAccessor.getLocaleID();
128         Set<String> items = new TreeSet<>();
129         Set<String> orderErrors = checkForErrorsAndGetLocales(localeID, value, items);
130         if (orderErrors != null) {
131             result.add(
132                     new CheckStatus()
133                             .setCause(checkAccessor)
134                             .setMainType(CheckStatus.errorType)
135                             .setSubtype(Subtype.invalidLocale)
136                             .setMessage("Invalid locales: " + JOIN_SPACE.join(orderErrors)));
137             return;
138         }
139         // Check to see that user's language and und are explicitly mentioned.
140         // but only if the value is not inherited.
141         String unresolvedValue = checkAccessor.getUnresolvedStringValue(path);
142         if (unresolvedValue != null) {
143             // And the other value is not inherited.
144             String otherPath =
145                     path.contains("givenFirst")
146                             ? path.replace("givenFirst", "surnameFirst")
147                             : path.replace("surnameFirst", "givenFirst");
148             String otherValue = checkAccessor.getStringValue(otherPath);
149             if (otherValue != null) {
150                 String myLanguage = localeID;
151                 if (!myLanguage.equals("root")) { // skip root
152 
153                     Set<String> items2 = new TreeSet<>();
154                     orderErrors =
155                             checkForErrorsAndGetLocales(
156                                     localeID,
157                                     otherValue,
158                                     items2); // adds locales from other path. We don't check for
159                     // errors there.
160                     if (!Collections.disjoint(items, items2)) {
161                         result.add(
162                                 new CheckStatus()
163                                         .setCause(checkAccessor)
164                                         .setMainType(CheckStatus.errorType)
165                                         .setSubtype(Subtype.invalidLocale)
166                                         .setMessage(
167                                                 "Locale codes can occur only once: "
168                                                         + JOIN_SPACE.join(
169                                                                 Sets.intersection(items, items2))));
170                     }
171 
172                     items.addAll(items2); // get the union for checking below
173                     myLanguage = new LocaleIDParser().set(myLanguage).getLanguage();
174 
175                     if (!items.contains(myLanguage)) {
176                         result.add(
177                                 new CheckStatus()
178                                         .setCause(checkAccessor)
179                                         .setMainType(CheckStatus.errorType)
180                                         .setSubtype(Subtype.missingLanguage)
181                                         .setMessage(
182                                                 "Your locale code ("
183                                                         + myLanguage
184                                                         + ") must be explicitly listed in one of the nameOrderLocales:"
185                                                         + " either in givenFirst or in surnameFirst."));
186                     }
187 
188                     if (!items.contains(LocaleNames.UND)) {
189                         result.add(
190                                 new CheckStatus()
191                                         .setCause(checkAccessor)
192                                         .setMainType(CheckStatus.errorType)
193                                         .setSubtype(Subtype.missingLanguage)
194                                         .setMessage(
195                                                 "The special code ‘und’ must be explicitly listed in one of the nameOrderLocales: either givenFirst or surnameFirst."));
196                     }
197                 }
198             }
199         }
200     }
201 
202     /**
203      * Verify the that sampleName items are clean.
204      *
205      * @param checkAccessor
206      */
checkSampleNames( CheckAccessor checkAccessor, XPathParts pathParts, String value, List<CheckStatus> result)207     public static void checkSampleNames(
208             CheckAccessor checkAccessor,
209             XPathParts pathParts,
210             String value,
211             List<CheckStatus> result) {
212         // ldml/personNames/sampleName[@item="informal"]/nameField[@type="surname"]
213 
214         // check basic consistency of modifier set
215         ModifiedField fieldType = ModifiedField.from(pathParts.getAttributeValue(-1, "type"));
216         Field field = fieldType.getField();
217         Set<Modifier> modifiers = fieldType.getModifiers();
218         Output<String> errorMessage = new Output<>();
219         Modifier.getCleanSet(modifiers, errorMessage);
220         final Type mainType =
221                 checkAccessor.getPhase() != Phase.BUILD
222                         ? CheckStatus.errorType
223                         : CheckStatus.warningType;
224         if (errorMessage.value != null) {
225             result.add(
226                     new CheckStatus()
227                             .setCause(checkAccessor)
228                             .setMainType(mainType)
229                             .setSubtype(Subtype.invalidPlaceHolder)
230                             .setMessage(errorMessage.value));
231             return;
232         }
233 
234         if (value.equals("∅∅∅")) {
235             // check for required values
236 
237             switch (field) {
238                 case given:
239                     // we must have a given
240                     if (fieldType.getModifiers().isEmpty()) {
241                         result.add(
242                                 new CheckStatus()
243                                         .setCause(checkAccessor)
244                                         .setMainType(mainType)
245                                         .setSubtype(Subtype.invalidPlaceHolder)
246                                         .setMessage(
247                                                 "Names must have a value for the ‘given‘ field. Mononyms (like ‘Zendaya’) use given, not surname"));
248                     }
249                     break;
250                 case surname:
251                     // can't have surname2 unless we have surname
252                     final XPathParts thawedPathParts = pathParts.cloneAsThawed();
253                     String modPath =
254                             thawedPathParts
255                                     .setAttribute(-1, "type", Field.surname2.toString())
256                                     .toString();
257                     String surname2Value = checkAccessor.getStringValue(modPath);
258                     String modPathcore =
259                             thawedPathParts.setAttribute(-1, "type", "surname-core").toString();
260                     String surnameCoreValue = checkAccessor.getStringValue(modPathcore);
261                     if (surname2Value != null
262                             && !surname2Value.equals("∅∅∅")
263                             && (surnameCoreValue == null || surnameCoreValue.equals("∅∅∅"))) {
264                         result.add(
265                                 new CheckStatus()
266                                         .setCause(checkAccessor)
267                                         .setMainType(CheckStatus.errorType)
268                                         .setSubtype(Subtype.invalidPlaceHolder)
269                                         .setMessage(
270                                                 "Names must have a value for the ‘surname’ field if they have a ‘surname2’ field."));
271                     }
272                     break;
273                 default:
274                     break;
275             }
276         } else if (value.equals(LocaleNames.ZXX)) { // mistaken "we don't use this"
277             result.add(
278                     new CheckStatus()
279                             .setCause(checkAccessor)
280                             .setMainType(CheckStatus.errorType)
281                             .setSubtype(Subtype.invalidPlaceHolder)
282                             .setMessage(
283                                     "Illegal name field; zxx is only appropriate for NameOrder locales"));
284         } else { // real value
285             // special checks for prefix/core
286             final boolean hasPrefix = modifiers.contains(Modifier.prefix);
287             final boolean hasCore = modifiers.contains(Modifier.core);
288             if (hasPrefix || hasCore) {
289                 // We need consistency among the 3 values if we have either prefix or core
290 
291                 String coreValue =
292                         hasCore
293                                 ? value
294                                 : modifiedFieldValue(
295                                         checkAccessor, pathParts, field, modifiers, Modifier.core);
296                 String prefixValue =
297                         hasPrefix
298                                 ? value
299                                 : modifiedFieldValue(
300                                         checkAccessor,
301                                         pathParts,
302                                         field,
303                                         modifiers,
304                                         Modifier.prefix);
305                 String plainValue =
306                         modifiedFieldValue(checkAccessor, pathParts, field, modifiers, null);
307 
308                 String errorMessage2 =
309                         Modifier.inconsistentPrefixCorePlainValues(
310                                 prefixValue, coreValue, plainValue);
311                 if (errorMessage2 != null) {
312                     result.add(
313                             new CheckStatus()
314                                     .setCause(checkAccessor)
315                                     .setMainType(CheckStatus.errorType)
316                                     .setSubtype(Subtype.invalidPlaceHolder)
317                                     .setMessage(errorMessage2));
318                 }
319             }
320         }
321     }
322 
323     static final ImmutableSet<Object> givenFirstSortingLocales =
324             ImmutableSet.of("is", "ta", "si"); // TODO should be data-driven
325 
326     /**
327      * Verify the that personName patterns are clean.
328      *
329      * @param path TODO
330      * @deprecated Use {@link
331      *     #checkPersonNamePatterns(CheckAccessor,Set<Modifier>,String,String,XPathParts,String,List<CheckStatus>)}
332      *     instead
333      */
334     @Deprecated
checkPersonNamePatterns( CheckAccessor checkAccessor, Set<Modifier> allowedModifiers, String path, XPathParts pathParts, String value, List<CheckStatus> result)335     public static void checkPersonNamePatterns(
336             CheckAccessor checkAccessor,
337             Set<Modifier> allowedModifiers,
338             String path,
339             XPathParts pathParts,
340             String value,
341             List<CheckStatus> result) {
342         checkPersonNamePatterns(
343                 checkAccessor, allowedModifiers, "fr", path, pathParts, value, result);
344     }
345 
346     /**
347      * Verify the that personName patterns are clean.
348      *
349      * @param locale TODO
350      * @param path TODO
351      */
checkPersonNamePatterns( CheckAccessor checkAccessor, Set<Modifier> allowedModifiers, String locale, String path, XPathParts pathParts, String value, List<CheckStatus> result)352     public static void checkPersonNamePatterns(
353             CheckAccessor checkAccessor,
354             Set<Modifier> allowedModifiers,
355             String locale,
356             String path,
357             XPathParts pathParts,
358             String value,
359             List<CheckStatus> result) {
360         // ldml/personNames/personName[@order="sorting"][@length="long"][@usage="addressing"][@style="formal"]/namePattern
361 
362         // check that the name pattern is valid
363 
364         Pair<FormatParameters, NamePattern> pair = null;
365         try {
366             pair = PersonNameFormatter.fromPathValue(pathParts, value);
367         } catch (Exception e) {
368             result.add(
369                     new CheckStatus()
370                             .setCause(checkAccessor)
371                             .setMainType(CheckStatus.errorType)
372                             .setSubtype(Subtype.invalidPlaceHolder)
373                             .setMessage("Invalid placeholder in value: «" + value + "»"));
374             return; // fatal error, don't bother with others
375         }
376 
377         final FormatParameters parameterMatcher = pair.getFirst();
378         final NamePattern namePattern = pair.getSecond();
379 
380         // now check that the namePattern is reasonable
381 
382         Multimap<Field, Integer> fieldToPositions = namePattern.getFieldPositions();
383 
384         // Check for special cases: https://unicode-org.atlassian.net/browse/CLDR-15782
385 
386         boolean usageIsMonogram =
387                 parameterMatcher.matches(new FormatParameters(null, null, Usage.monogram, null));
388 
389         ModifiedField lastModifiedField = null;
390         for (int i = 0; i < namePattern.getElementCount(); ++i) {
391             ModifiedField modifiedField = namePattern.getModifiedField(i);
392             if (modifiedField == null) { // literal
393                 String literal = namePattern.getLiteral(i);
394                 if (literal.contains(".")) {
395                     if (lastModifiedField != null) {
396                         Set<Modifier> lastModifiers = lastModifiedField.getModifiers();
397                         if (lastModifiers.contains(Modifier.initial)
398                                 && lastModifiers.contains(Modifier.initialCap)) {
399                             result.add(
400                                     new CheckStatus()
401                                             .setCause(checkAccessor)
402                                             .setMainType(CheckStatus.warningType)
403                                             .setSubtype(Subtype.namePlaceholderProblem)
404                                             .setMessage(
405                                                     "“.” is strongly discouraged after an -initial or -initialCap placeholder in {"
406                                                             + lastModifiedField
407                                                             + "}"));
408                             continue;
409                         }
410                     }
411                     if (usageIsMonogram) {
412                         result.add(
413                                 new CheckStatus()
414                                         .setCause(checkAccessor)
415                                         .setMainType(CheckStatus.warningType)
416                                         .setSubtype(Subtype.namePlaceholderProblem)
417                                         .setMessage(
418                                                 "“.” is discouraged when usage=monogram, in "
419                                                         + namePattern));
420                     }
421                 }
422             } else {
423                 lastModifiedField = modifiedField;
424                 Set<Modifier> modifiers = modifiedField.getModifiers();
425                 Field field = modifiedField.getField();
426                 if (!allowedModifiers.containsAll(modifiers)) {
427                     result.add(
428                             new CheckStatus()
429                                     .setCause(checkAccessor)
430                                     .setMainType(CheckStatus.errorType)
431                                     .setSubtype(Subtype.invalidPlaceHolder)
432                                     .setMessage(
433                                             "Illegal grammatical case modifiers for {0}: {1}",
434                                             locale, Sets.difference(modifiers, allowedModifiers)));
435                 }
436                 switch (field) {
437                     case title:
438                     case credentials:
439                     case generation:
440                         if (usageIsMonogram) {
441                             result.add(
442                                     new CheckStatus()
443                                             .setCause(checkAccessor)
444                                             .setMainType(CheckStatus.errorType)
445                                             .setSubtype(Subtype.invalidPlaceHolder)
446                                             .setMessage(
447                                                     "Disallowed when usage=monogram: {"
448                                                             + field
449                                                             + "…}"));
450                         }
451                         break;
452                     default:
453                         final boolean monogramModifier = modifiers.contains(Modifier.monogram);
454                         final boolean allCapsModifier = modifiers.contains(Modifier.allCaps);
455                         if (!usageIsMonogram) {
456                             if (monogramModifier) {
457                                 result.add(
458                                         new CheckStatus()
459                                                 .setCause(checkAccessor)
460                                                 .setMainType(CheckStatus.warningType)
461                                                 .setSubtype(Subtype.invalidPlaceHolder)
462                                                 .setMessage(
463                                                         "-monogram is strongly discouraged when usage≠monogram, in {"
464                                                                 + modifiedField
465                                                                 + "}"));
466                             }
467                         } else if (usageIsMonogram) {
468                             if (!monogramModifier) {
469                                 result.add(
470                                         new CheckStatus()
471                                                 .setCause(checkAccessor)
472                                                 .setMainType(CheckStatus.errorType)
473                                                 .setSubtype(Subtype.invalidPlaceHolder)
474                                                 .setMessage(
475                                                         "-monogram is required when usage=monogram, in {"
476                                                                 + modifiedField
477                                                                 + "}"));
478                             } else if (!allCapsModifier) {
479                                 result.add(
480                                         new CheckStatus()
481                                                 .setCause(checkAccessor)
482                                                 .setMainType(CheckStatus.warningType)
483                                                 .setSubtype(Subtype.invalidPlaceHolder)
484                                                 .setMessage(
485                                                         "-allCaps is strongly encouraged with -monogram, in {"
486                                                                 + modifiedField
487                                                                 + "}"));
488                             }
489                         }
490                 }
491             }
492             lastModifiedField = modifiedField;
493         }
494 
495         // gather information about the fields
496         int firstSurname = Integer.MAX_VALUE;
497         int firstGiven = Integer.MAX_VALUE;
498 
499         // TODO ALL check for combinations we should enforce; eg, only have given2 if there is a
500         // given; only have surname2 if there is a surname; others?
501 
502         for (Entry<Field, Collection<Integer>> entry : fieldToPositions.asMap().entrySet()) {
503 
504             // If a field occurs twice, probably an error. Could relax this upon feedback
505 
506             Collection<Integer> positions = entry.getValue();
507             if (positions.size() > 1) {
508 
509                 // However, do allow prefix&core together
510 
511                 boolean skip = false;
512                 if (entry.getKey() == Field.surname) {
513                     Iterator<Integer> it = positions.iterator();
514                     Set<Modifier> m1 = namePattern.getModifiedField(it.next()).getModifiers();
515                     Set<Modifier> m2 = namePattern.getModifiedField(it.next()).getModifiers();
516                     skip =
517                             m1.contains(Modifier.core) && m2.contains(Modifier.prefix)
518                                     || m1.contains(Modifier.prefix) && m2.contains(Modifier.core);
519                 }
520 
521                 if (!skip) {
522                     result.add(
523                             new CheckStatus()
524                                     .setCause(checkAccessor)
525                                     .setMainType(CheckStatus.errorType)
526                                     .setSubtype(Subtype.invalidPlaceHolder)
527                                     .setMessage("Duplicate fields: " + entry));
528                 }
529             }
530 
531             // gather some info for later
532 
533             Integer leastPosition = positions.iterator().next();
534             switch (entry.getKey()) {
535                 case given:
536                 case given2:
537                     firstGiven = Math.min(leastPosition, firstGiven);
538                     break;
539                 case surname:
540                 case surname2:
541                     firstSurname = Math.min(leastPosition, firstSurname);
542                     break;
543                 default: // ignore
544             }
545         }
546 
547         // the rest of the tests are of the pattern, and only apply when we have both given and
548         // surname
549         // and not inheriting
550 
551         if (firstGiven < Integer.MAX_VALUE
552                 && firstSurname < Integer.MAX_VALUE
553                 && checkAccessor.getUnresolvedStringValue(path) != null) {
554 
555             Order orderRaw = parameterMatcher.getOrder();
556             Set<Order> order = orderRaw == null ? Order.ALL : ImmutableSet.of(orderRaw);
557             // TODO, fix to avoid set (a holdover from using PatternMatcher)
558 
559             // Handle 'sorting' value. Will usually be compatible with surnameFirst in foundOrder,
560             // except for known exceptions
561 
562             if (order.contains(Order.sorting)) {
563                 EnumSet<Order> temp = EnumSet.noneOf(Order.class);
564                 temp.addAll(order);
565                 temp.remove(Order.sorting);
566                 if (givenFirstSortingLocales.contains(
567                         checkAccessor
568                                 .getLocaleID())) { // TODO Mark cover contains-by-inheritance also
569                     temp.add(Order.givenFirst);
570                 } else {
571                     temp.add(Order.surnameFirst);
572                 }
573                 order = temp;
574             }
575 
576             if (order.isEmpty()) {
577                 order = Order.ALL;
578             }
579 
580             // check that we don't have a difference in the order AND there is a surname or surname2
581             // that is, it is ok to coalesce patterns of different orders where the order doesn't
582             // make a difference
583 
584             { // TODO: clean up to avoid block
585                 if (order.contains(Order.givenFirst) && order.contains(Order.surnameFirst)) {
586                     result.add(
587                             new CheckStatus()
588                                     .setCause(checkAccessor)
589                                     .setMainType(CheckStatus.errorType)
590                                     .setSubtype(Subtype.invalidPlaceHolder)
591                                     .setMessage("Conflicting Order values: " + order));
592                 }
593 
594                 // now check order in pattern is consistent with Order
595 
596                 Order foundOrder =
597                         firstGiven < firstSurname ? Order.givenFirst : Order.surnameFirst;
598                 final Order first = order.iterator().next();
599 
600                 if (first != foundOrder) {
601 
602                     //                    if (first == Order.givenFirst &&
603                     // !"en".equals(checkAccessor.getLocaleID())) { // TODO Mark Drop HACK once root
604                     // is ok
605                     //                        return;
606                     //                    }
607 
608                     result.add(
609                             new CheckStatus()
610                                     .setCause(checkAccessor)
611                                     .setMainType(CheckStatus.errorType)
612                                     .setSubtype(Subtype.invalidPlaceHolder)
613                                     .setMessage(
614                                             "Pattern order {0} is inconsistent with code order {1}",
615                                             foundOrder, first));
616                 }
617             }
618         }
619     }
620 
621     /** Check that {\d+} placeholders are ok; no unterminated, only digits */
622     private void checkBasicPlaceholders(String value, List<CheckStatus> result) {
623         int startPlaceHolder = 0;
624         int endPlaceHolder;
625         while (startPlaceHolder != -1 && startPlaceHolder < value.length()) {
626             startPlaceHolder = value.indexOf('{', startPlaceHolder + 1);
627             if (startPlaceHolder != -1) {
628                 endPlaceHolder = value.indexOf('}', startPlaceHolder + 1);
629                 if (endPlaceHolder == -1) {
630                     result.add(
631                             new CheckStatus()
632                                     .setCause(this)
633                                     .setMainType(CheckStatus.errorType)
634                                     .setSubtype(Subtype.invalidPlaceHolder)
635                                     .setMessage(
636                                             "Invalid placeholder (missing terminator) in value «"
637                                                     + value
638                                                     + "»"));
639                 } else {
640                     String placeHolderString =
641                             value.substring(startPlaceHolder + 1, endPlaceHolder);
642                     Matcher matcher = PLACEHOLDER_PATTERN.matcher(placeHolderString);
643                     if (!matcher.matches()) {
644                         result.add(
645                                 new CheckStatus()
646                                         .setCause(this)
647                                         .setMainType(CheckStatus.errorType)
648                                         .setSubtype(Subtype.invalidPlaceHolder)
649                                         .setMessage(
650                                                 "Invalid placeholder (contents \""
651                                                         + placeHolderString
652                                                         + "\") in value \""
653                                                         + value
654                                                         + "\""));
655                     }
656                     startPlaceHolder = endPlaceHolder;
657                 }
658             }
659         }
660     }
661 
662     /** Check that list patterns are "ordered" so that they only compose from the right. */
663     private void checkListPatterns(String path, String value, List<CheckStatus> result) {
664         // eg
665         // ldml/listPatterns/listPattern/listPatternPart[@type="start"]
666         // ldml/listPatterns/listPattern[@type="standard-short"]/listPatternPart[@type="2"]
667         if (path.startsWith("//ldml/listPatterns/listPattern")) {
668             XPathParts parts = XPathParts.getFrozenInstance(path);
669             // check order, {0} must be before {1}
670 
671             switch (parts.getAttributeValue(-1, "type")) {
672                 case "start":
673                     checkNothingAfter1(value, result);
674                     break;
675                 case "middle":
676                     checkNothingBefore0(value, result);
677                     checkNothingAfter1(value, result);
678                     break;
679                 case "end":
680                     checkNothingBefore0(value, result);
681                     break;
682                 case "2":
683                     {
684                         int pos1 = value.indexOf("{0}");
685                         int pos2 = value.indexOf("{1}");
686                         if (pos1 > pos2) {
687                             result.add(
688                                     new CheckStatus()
689                                             .setCause(this)
690                                             .setMainType(CheckStatus.errorType)
691                                             .setSubtype(Subtype.invalidPlaceHolder)
692                                             .setMessage(
693                                                     "Invalid list pattern «"
694                                                             + value
695                                                             + "»: the placeholder {0} must be before {1}."));
696                         }
697                     }
698                     break;
699                 case "3":
700                     {
701                         int pos1 = value.indexOf("{0}");
702                         int pos2 = value.indexOf("{1}");
703                         int pos3 = value.indexOf("{2}");
704                         if (pos1 > pos2 || pos2 > pos3) {
705                             result.add(
706                                     new CheckStatus()
707                                             .setCause(this)
708                                             .setMainType(CheckStatus.errorType)
709                                             .setSubtype(Subtype.invalidPlaceHolder)
710                                             .setMessage(
711                                                     "Invalid list pattern «"
712                                                             + value
713                                                             + "»: the placeholders {0}, {1}, {2} must appear in that order."));
714                         }
715                     }
716                     break;
717             }
718         }
719     }
720 
721     /** Check that both patterns don't have the same literals. */
checkInitialPattern( CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result)722     public static void checkInitialPattern(
723             CheckAccessor checkAccessor, String path, String value, List<CheckStatus> result) {
724         if (path.contains("initialSequence")) {
725             String valueLiterals = value.replace("{0}", "").replace("{1}", "");
726             if (!valueLiterals.isBlank()) {
727                 String otherPath = path.replace("initialSequence", "initial");
728                 String otherValue = checkAccessor.getStringValue(otherPath);
729                 if (otherValue != null) {
730                     String literals = otherValue.replace("{0}", "");
731                     if (!literals.isBlank() && value.contains(literals)) {
732                         result.add(
733                                 new CheckStatus()
734                                         .setCause(checkAccessor)
735                                         .setMainType(CheckStatus.errorType)
736                                         .setSubtype(Subtype.namePlaceholderProblem)
737                                         .setMessage(
738                                                 "The initialSequence pattern must not contain initial pattern literals: «"
739                                                         + literals
740                                                         + "»"));
741                         return;
742                     }
743                 }
744                 result.add(
745                         new CheckStatus()
746                                 .setCause(checkAccessor)
747                                 .setMainType(CheckStatus.warningType)
748                                 .setSubtype(Subtype.namePlaceholderProblem)
749                                 .setMessage(
750                                         "Non-space characters are discouraged in the initialSequence pattern: «"
751                                                 + valueLiterals.replace(" ", "")
752                                                 + "»"));
753             }
754         }
755         // no current check for the type="initial"
756     }
757 
758     static final UnicodeSet allowedForeignSpaceReplacements =
759             new UnicodeSet("[[:whitespace:][:punctuation:]]");
760 
761     /** Check that the value is limited to punctuation or space, or inherits */
checkForeignSpaceReplacement( CheckAccessor checkAccessor, String value, List<CheckStatus> result)762     public static void checkForeignSpaceReplacement(
763             CheckAccessor checkAccessor, String value, List<CheckStatus> result) {
764         if (!allowedForeignSpaceReplacements.containsAll(value) && !value.equals("↑↑↑")) {
765             result.add(
766                     new CheckStatus()
767                             .setCause(checkAccessor)
768                             .setMainType(CheckStatus.errorType)
769                             .setSubtype(Subtype.invalidLocale)
770                             .setMessage(
771                                     "Invalid choice, must be punctuation or a space: «"
772                                             + value
773                                             + "»"));
774         }
775     }
776 
777     /** Gets a string value for a modified path */
modifiedFieldValue( CheckAccessor checkAccessor, XPathParts parts, Field field, Set<Modifier> modifiers, Modifier toAdd)778     private static String modifiedFieldValue(
779             CheckAccessor checkAccessor,
780             XPathParts parts,
781             Field field,
782             Set<Modifier> modifiers,
783             Modifier toAdd) {
784         Set<Modifier> adjustedModifiers = Sets.difference(modifiers, CORE_AND_PREFIX);
785         if (toAdd != null) {
786             switch (toAdd) {
787                 case core:
788                     adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_CORE);
789                     break;
790                 case prefix:
791                     adjustedModifiers = Sets.union(adjustedModifiers, SINGLE_PREFIX);
792                     break;
793                 default:
794                     break;
795             }
796         }
797         String modPath =
798                 parts.cloneAsThawed()
799                         .setAttribute(
800                                 -1, "type", new ModifiedField(field, adjustedModifiers).toString())
801                         .toString();
802         String value = checkAccessor.getStringValue(modPath);
803         return "∅∅∅".equals(value) ? null : value;
804     }
805 
checkForErrorsAndGetLocales( String locale, String value, Set<String> items)806     public static Set<String> checkForErrorsAndGetLocales(
807             String locale, String value, Set<String> items) {
808         if (value.isEmpty()) {
809             return null;
810         }
811         Set<String> orderErrors = null;
812         for (String item : SPLIT_SPACE.split(value)) {
813             boolean mv = (item.equals(locale)) || CLDR_LOCALES_FOR_NAME_ORDER.contains(item);
814             if (!mv) {
815                 if (orderErrors == null) {
816                     orderErrors = new LinkedHashSet<>();
817                 }
818                 orderErrors.add(item);
819             } else {
820                 items.add(item);
821             }
822         }
823         return orderErrors;
824     }
825 
checkNothingAfter1(String value, List<CheckStatus> result)826     private void checkNothingAfter1(String value, List<CheckStatus> result) {
827         if (!value.endsWith("{1}")) {
828             result.add(
829                     new CheckStatus()
830                             .setCause(this)
831                             .setMainType(CheckStatus.errorType)
832                             .setSubtype(Subtype.invalidPlaceHolder)
833                             .setMessage(
834                                     "Invalid list pattern «"
835                                             + value
836                                             + "», no text can come after {1}."));
837         }
838     }
839 
checkNothingBefore0(String value, List<CheckStatus> result)840     private void checkNothingBefore0(String value, List<CheckStatus> result) {
841         if (!value.startsWith("{0}")) {
842             result.add(
843                     new CheckStatus()
844                             .setCause(this)
845                             .setMainType(CheckStatus.errorType)
846                             .setSubtype(Subtype.invalidPlaceHolder)
847                             .setMessage(
848                                     "Invalid list pattern «"
849                                             + value
850                                             + "», no text can come before {0}."));
851         }
852     }
853 }
854