diff --git a/icu4c/source/common/unicode/utypes.h b/icu4c/source/common/unicode/utypes.h index ba27be222349..fcec29546af7 100644 --- a/icu4c/source/common/unicode/utypes.h +++ b/icu4c/source/common/unicode/utypes.h @@ -599,12 +599,13 @@ typedef enum UErrorCode { U_MF_OPERAND_MISMATCH_ERROR, /**< An operand provided to a function does not have the required form for that function @internal ICU 75 technology preview @deprecated This API is for technology preview only. */ U_MF_UNSUPPORTED_STATEMENT_ERROR, /**< A message includes a reserved statement. @internal ICU 75 technology preview @deprecated This API is for technology preview only. */ U_MF_UNSUPPORTED_EXPRESSION_ERROR, /**< A message includes syntax reserved for future standardization or private implementation use. @internal ICU 75 technology preview @deprecated This API is for technology preview only. */ + U_MF_DUPLICATE_VARIANT_ERROR, /**< A message includes a variant with the same key list as another variant. @internal ICU 76 technology preview @deprecated This API is for technology preview only. */ #ifndef U_HIDE_DEPRECATED_API /** * One more than the highest normal formatting API error code. * @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420. */ - U_FMT_PARSE_ERROR_LIMIT = 0x10121, + U_FMT_PARSE_ERROR_LIMIT = 0x10122, #endif // U_HIDE_DEPRECATED_API /* diff --git a/icu4c/source/common/utypes.cpp b/icu4c/source/common/utypes.cpp index 715994d67f04..2f449ffea033 100644 --- a/icu4c/source/common/utypes.cpp +++ b/icu4c/source/common/utypes.cpp @@ -141,7 +141,8 @@ _uFmtErrorName[U_FMT_PARSE_ERROR_LIMIT - U_FMT_PARSE_ERROR_START] = { "U_MF_DUPLICATE_DECLARATION_ERROR", "U_MF_OPERAND_MISMATCH_ERROR", "U_MF_UNSUPPORTED_STATEMENT_ERROR", - "U_MF_UNSUPPORTED_EXPRESSION_ERROR" + "U_MF_UNSUPPORTED_EXPRESSION_ERROR", + "U_MF_DUPLICATE_VARIANT_ERROR" }; static const char * const diff --git a/icu4c/source/i18n/messageformat2_checker.cpp b/icu4c/source/i18n/messageformat2_checker.cpp index 192167583fff..824b798410d5 100644 --- a/icu4c/source/i18n/messageformat2_checker.cpp +++ b/icu4c/source/i18n/messageformat2_checker.cpp @@ -22,6 +22,7 @@ Checks data model errors The following are checked here: Variant Key Mismatch +Duplicate Variant Missing Fallback Variant (called NonexhaustivePattern here) Missing Selector Annotation Duplicate Declaration @@ -162,6 +163,7 @@ void Checker::checkVariants(UErrorCode& status) { // Check that one variant includes only wildcards bool defaultExists = false; + bool duplicatesExist = false; for (int32_t i = 0; i < dataModel.numVariants(); i++) { const SelectorKeys& k = variants[i].getKeys(); @@ -173,10 +175,35 @@ void Checker::checkVariants(UErrorCode& status) { return; } defaultExists |= areDefaultKeys(keys, len); + + // Check if this variant's keys are duplicated by any other variant's keys + if (!duplicatesExist) { + // This check takes quadratic time, but it can be optimized if checking + // this property turns out to be a bottleneck. + for (int32_t j = 0; j < i; j++) { + const SelectorKeys& k1 = variants[j].getKeys(); + const Key* keys1 = k1.getKeysInternal(); + bool allEqual = true; + // This variant was already checked, + // so we know keys1.len == len + for (int32_t kk = 0; kk < len; kk++) { + if (!(keys[kk] == keys1[kk])) { + allEqual = false; + break; + } + } + if (allEqual) { + duplicatesExist = true; + } + } + } + } + + if (duplicatesExist) { + errors.addError(StaticErrorType::DuplicateVariant, status); } if (!defaultExists) { errors.addError(StaticErrorType::NonexhaustivePattern, status); - return; } } diff --git a/icu4c/source/i18n/messageformat2_data_model.cpp b/icu4c/source/i18n/messageformat2_data_model.cpp index f7368b3808b8..a8762e4004f6 100644 --- a/icu4c/source/i18n/messageformat2_data_model.cpp +++ b/icu4c/source/i18n/messageformat2_data_model.cpp @@ -186,6 +186,9 @@ bool Key::operator==(const Key& other) const { if (isWildcard()) { return other.isWildcard(); } + if (other.isWildcard()) { + return false; + } return (asLiteral() == other.asLiteral()); } @@ -833,23 +836,19 @@ const Expression& Binding::getValue() const { } else { const Operator* rator = rhs.getOperator(errorCode); bool hasOperator = U_SUCCESS(errorCode); - if (hasOperator && rator->isReserved()) { - errorCode = U_INVALID_STATE_ERROR; + // Clear error code -- the "error" from the absent operator + // is handled + errorCode = U_ZERO_ERROR; + b = Binding(variableName, std::move(rhs)); + b.local = false; + if (hasOperator) { + rator = b.getValue().getOperator(errorCode); + U_ASSERT(U_SUCCESS(errorCode)); + b.annotation = &rator->contents; } else { - // Clear error code -- the "error" from the absent operator - // is handled - errorCode = U_ZERO_ERROR; - b = Binding(variableName, std::move(rhs)); - b.local = false; - if (hasOperator) { - rator = b.getValue().getOperator(errorCode); - U_ASSERT(U_SUCCESS(errorCode)); - b.annotation = std::get_if(&(rator->contents)); - } else { - b.annotation = nullptr; - } - U_ASSERT(!hasOperator || b.annotation != nullptr); + b.annotation = nullptr; } + U_ASSERT(!hasOperator || b.annotation != nullptr); } } return b; @@ -857,7 +856,8 @@ const Expression& Binding::getValue() const { const OptionMap& Binding::getOptionsInternal() const { U_ASSERT(annotation != nullptr); - return annotation->getOptions(); + U_ASSERT(std::holds_alternative(*annotation)); + return std::get_if(annotation)->getOptions(); } void Binding::updateAnnotation() { @@ -867,7 +867,7 @@ void Binding::updateAnnotation() { return; } U_ASSERT(U_SUCCESS(localErrorCode) && !rator->isReserved()); - annotation = std::get_if(&(rator->contents)); + annotation = &rator->contents; } Binding::Binding(const Binding& other) : var(other.var), expr(other.expr), local(other.local) { diff --git a/icu4c/source/i18n/messageformat2_errors.cpp b/icu4c/source/i18n/messageformat2_errors.cpp index cbb9e1497d69..1fb31377cd1c 100644 --- a/icu4c/source/i18n/messageformat2_errors.cpp +++ b/icu4c/source/i18n/messageformat2_errors.cpp @@ -135,6 +135,10 @@ namespace message2 { status = U_MF_VARIANT_KEY_MISMATCH_ERROR; break; } + case StaticErrorType::DuplicateVariant: { + status = U_MF_DUPLICATE_VARIANT_ERROR; + break; + } case StaticErrorType::NonexhaustivePattern: { status = U_MF_NONEXHAUSTIVE_PATTERN_ERROR; break; @@ -211,6 +215,10 @@ namespace message2 { dataModelError = true; break; } + case StaticErrorType::DuplicateVariant: { + dataModelError = true; + break; + } case StaticErrorType::NonexhaustivePattern: { dataModelError = true; break; diff --git a/icu4c/source/i18n/messageformat2_errors.h b/icu4c/source/i18n/messageformat2_errors.h index ef2ad20faddb..604c8bcf57ac 100644 --- a/icu4c/source/i18n/messageformat2_errors.h +++ b/icu4c/source/i18n/messageformat2_errors.h @@ -54,6 +54,7 @@ namespace message2 { enum StaticErrorType { DuplicateDeclarationError, DuplicateOptionName, + DuplicateVariant, MissingSelectorAnnotation, NonexhaustivePattern, SyntaxError, diff --git a/icu4c/source/i18n/messageformat2_function_registry.cpp b/icu4c/source/i18n/messageformat2_function_registry.cpp index e9bbc03e737e..17955760ecfb 100644 --- a/icu4c/source/i18n/messageformat2_function_registry.cpp +++ b/icu4c/source/i18n/messageformat2_function_registry.cpp @@ -7,11 +7,14 @@ #if !UCONFIG_NO_MF2 +#include + #include "unicode/dtptngen.h" #include "unicode/messageformat2_data_model_names.h" #include "unicode/messageformat2_function_registry.h" #include "unicode/smpdtfmt.h" #include "charstr.h" +#include "double-conversion.h" #include "messageformat2_allocation.h" #include "messageformat2_function_registry_internal.h" #include "messageformat2_macros.h" @@ -421,12 +424,11 @@ static FormattedPlaceholder notANumber(const FormattedPlaceholder& input) { return FormattedPlaceholder(input, FormattedValue(UnicodeString("NaN"))); } -static FormattedPlaceholder stringAsNumber(const number::LocalizedNumberFormatter& nf, const FormattedPlaceholder& input, UErrorCode& errorCode) { +static double parseNumberLiteral(const FormattedPlaceholder& input, UErrorCode& errorCode) { if (U_FAILURE(errorCode)) { return {}; } - double numberValue; // Copying string to avoid GCC dangling-reference warning // (although the reference is safe) UnicodeString inputStr = input.asFormattable().getString(errorCode); @@ -434,12 +436,39 @@ static FormattedPlaceholder stringAsNumber(const number::LocalizedNumberFormatte if (U_FAILURE(errorCode)) { return {}; } - UErrorCode localErrorCode = U_ZERO_ERROR; - strToDouble(inputStr, numberValue, localErrorCode); - if (U_FAILURE(localErrorCode)) { + + // Hack: Check for cases that are forbidden by the MF2 grammar + // but allowed by StringToDouble + int32_t len = inputStr.length(); + + if (len > 0 && ((inputStr[0] == '+') + || (inputStr[0] == '0' && len > 1 && inputStr[1] != '.') + || (inputStr[len - 1] == '.') + || (inputStr[0] == '.'))) { errorCode = U_MF_OPERAND_MISMATCH_ERROR; + return 0; + } + + // Otherwise, convert to double using double_conversion::StringToDoubleConverter + using namespace double_conversion; + int processedCharactersCount = 0; + StringToDoubleConverter converter(0, 0, 0, "", ""); + double result = + converter.StringToDouble(reinterpret_cast(inputStr.getBuffer()), + len, + &processedCharactersCount); + if (processedCharactersCount != len) { + errorCode = U_MF_OPERAND_MISMATCH_ERROR; + } + return result; +} + +static FormattedPlaceholder tryParsingNumberLiteral(const number::LocalizedNumberFormatter& nf, const FormattedPlaceholder& input, UErrorCode& errorCode) { + double numberValue = parseNumberLiteral(input, errorCode); + if (U_FAILURE(errorCode)) { return notANumber(input); } + UErrorCode savedStatus = errorCode; number::FormattedNumber result = nf.formatDouble(numberValue, errorCode); // Ignore U_USING_DEFAULT_WARNING @@ -590,7 +619,7 @@ FormattedPlaceholder StandardFunctions::Number::format(FormattedPlaceholder&& ar } case UFMT_STRING: { // Try to parse the string as a number - return stringAsNumber(realFormatter, arg, errorCode); + return tryParsingNumberLiteral(realFormatter, arg, errorCode); } default: { // Other types can't be parsed as a number diff --git a/icu4c/source/i18n/messageformat2_macros.h b/icu4c/source/i18n/messageformat2_macros.h index ee8cf0779e6b..f06ed1a5a977 100644 --- a/icu4c/source/i18n/messageformat2_macros.h +++ b/icu4c/source/i18n/messageformat2_macros.h @@ -60,19 +60,11 @@ using namespace pluralimpl; // Fallback #define REPLACEMENT ((UChar32) 0xFFFD) -// MessageFormat2 uses four keywords: `.input`, `.local`, `.when`, and `.match`. +// MessageFormat2 uses three keywords: `.input`, `.local`, and `.match`. -static constexpr UChar32 ID_INPUT[] = { - 0x2E, 0x69, 0x6E, 0x70, 0x75, 0x74, 0 /* ".input" */ -}; - -static constexpr UChar32 ID_LOCAL[] = { - 0x2E, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0 /* ".local" */ -}; - -static constexpr UChar32 ID_MATCH[] = { - 0x2E, 0x6D, 0x61, 0x74, 0x63, 0x68, 0 /* ".match" */ -}; +static constexpr std::u16string_view ID_INPUT = u".input"; +static constexpr std::u16string_view ID_LOCAL = u".local"; +static constexpr std::u16string_view ID_MATCH = u".match"; // Returns immediately if `errorCode` indicates failure #define CHECK_ERROR(errorCode) \ diff --git a/icu4c/source/i18n/messageformat2_parser.cpp b/icu4c/source/i18n/messageformat2_parser.cpp index fe5bfb484803..0635074f69c3 100644 --- a/icu4c/source/i18n/messageformat2_parser.cpp +++ b/icu4c/source/i18n/messageformat2_parser.cpp @@ -456,12 +456,11 @@ void Parser::parseToken(UChar32 c, UErrorCode& errorCode) { the string beginning at `source[index]` No postcondition -- a message can end with a '}' token */ -template -void Parser::parseToken(const UChar32 (&token)[N], UErrorCode& errorCode) { +void Parser::parseToken(const std::u16string_view& token, UErrorCode& errorCode) { U_ASSERT(inBounds(source, index)); int32_t tokenPos = 0; - while (tokenPos < N - 1) { + while (tokenPos < (int32_t) token.length()) { if (source[index] != token[tokenPos]) { ERROR(parseError, errorCode, index); return; @@ -478,13 +477,12 @@ void Parser::parseToken(const UChar32 (&token)[N], UErrorCode& errorCode) { the string beginning at `source[index']`), then consumes optional whitespace again */ -template -void Parser::parseTokenWithWhitespace(const UChar32 (&token)[N], UErrorCode& errorCode) { +void Parser::parseTokenWithWhitespace(const std::u16string_view& token, UErrorCode& errorCode) { // No need for error check or bounds check before parseOptionalWhitespace parseOptionalWhitespace(errorCode); // Establish precondition CHECK_BOUNDS(source, index, parseError, errorCode); - parseToken(token); + parseToken(token, errorCode); parseOptionalWhitespace(errorCode); // Guarantee postcondition CHECK_BOUNDS(source, index, parseError, errorCode); @@ -641,80 +639,40 @@ FunctionName Parser::parseFunction(UErrorCode& errorCode) { Precondition: source[index] == BACKSLASH Consume an escaped character. + Corresponds to `escaped-char` in the grammar. - Generalized to handle `reserved-escape`, `text-escape`, - or `literal-escape`, depending on the `kind` argument. - - Appends result to `str` + No postcondition (a message can end with an escaped char) */ -void Parser::parseEscapeSequence(EscapeKind kind, - UnicodeString &str, - UErrorCode& errorCode) { +UnicodeString Parser::parseEscapeSequence(UErrorCode& errorCode) { U_ASSERT(inBounds(source, index)); U_ASSERT(source[index] == BACKSLASH); normalizedInput += BACKSLASH; index++; // Skip the initial backslash - CHECK_BOUNDS(source, index, parseError, errorCode); - - #define SUCCEED \ - /* Append to the output string */ \ - str += source[index]; \ - /* Update normalizedInput */ \ - normalizedInput += source[index]; \ - /* Consume the character */ \ - index++; \ - /* Guarantee postcondition */ \ - CHECK_BOUNDS(source, index, parseError, errorCode); \ - return; - - // Expect a '{', '|' or '}' - switch (source[index]) { - case LEFT_CURLY_BRACE: - case RIGHT_CURLY_BRACE: { - // Allowed in a `text-escape` or `reserved-escape` - switch (kind) { - case TEXT: - case RESERVED: { - SUCCEED; + UnicodeString str; + if (inBounds(source, index)) { + // Expect a '{', '|' or '}' + switch (source[index]) { + case LEFT_CURLY_BRACE: + case RIGHT_CURLY_BRACE: + case PIPE: + case BACKSLASH: { + /* Append to the output string */ + str += source[index]; + /* Update normalizedInput */ + normalizedInput += source[index]; + /* Consume the character */ + index++; + return str; } default: { + // No other characters are allowed here break; } } - break; } - case PIPE: { - // Allowed in a `literal-escape` or `reserved-escape` - switch (kind) { - case LITERAL: - case RESERVED: { - SUCCEED; - } - default: { - break; - } - } - break; - } - case BACKSLASH: { - // Allowed in any escape sequence - SUCCEED; - } - default: { - // No other characters are allowed here - break; - } - } // If control reaches here, there was an error ERROR(parseError, errorCode, index); -} - -/* - Consume an escaped pipe or backslash, matching the `literal-escape` - nonterminal in the grammar -*/ -void Parser::parseLiteralEscape(UnicodeString &str, UErrorCode& errorCode) { - parseEscapeSequence(LITERAL, str, errorCode); + return str; } @@ -736,7 +694,7 @@ Literal Parser::parseQuotedLiteral(UErrorCode& errorCode) { bool done = false; while (!done) { if (source[index] == BACKSLASH) { - parseLiteralEscape(contents, errorCode); + contents += parseEscapeSequence(errorCode); } else if (isQuotedChar(source[index])) { contents += source[index]; normalizedInput += source[index]; @@ -1142,10 +1100,6 @@ Arbitrary lookahead is required to parse attribute lists, similarly to option li } } -void Parser::parseReservedEscape(UnicodeString &str, UErrorCode& errorCode) { - parseEscapeSequence(RESERVED, str, errorCode); -} - /* Consumes a non-empty sequence of reserved-chars, reserved-escapes, and literals (as in 1*(reserved-char / reserved-escape / literal) in the `reserved-body` rule) @@ -1177,8 +1131,7 @@ void Parser::parseReservedChunk(Reserved::Builder& result, UErrorCode& status) { if (source[index] == BACKSLASH) { // reserved-escape - parseReservedEscape(chunk, status); - result.add(Literal(false, chunk), status); + result.add(Literal(false, parseEscapeSequence(status)), status); chunk.setTo(u"", 0); } else if (source[index] == PIPE || isUnquotedStart(source[index])) { result.add(parseLiteral(status), status); @@ -1718,15 +1671,17 @@ void Parser::parseUnsupportedStatement(UErrorCode& status) { dataModel.addUnsupportedStatement(builder.build(status), status); } -// Terrible hack to get around the ambiguity between `matcher` and `reserved-statement` -bool Parser::nextIsMatch() const { - for(int32_t i = 0; i < 6; i++) { - if (!inBounds(source, index + i) || source[index + i] != ID_MATCH[i]) { +// Terrible hack to get around the ambiguity between unsupported keywords +// and supported keywords +bool Parser::nextIs(const std::u16string_view &keyword) const { + for(int32_t i = 0; i < (int32_t) keyword.length(); i++) { + if (!inBounds(source, index + i) || source[index + i] != keyword[i]) { return false; } } return true; } + /* Consume a possibly-empty sequence of declarations separated by whitespace; each declaration matches the `declaration` nonterminal in the grammar @@ -1740,19 +1695,17 @@ void Parser::parseDeclarations(UErrorCode& status) { while (source[index] == PERIOD) { CHECK_BOUNDS(source, index + 1, parseError, status); - if (source[index + 1] == ID_LOCAL[1]) { + // Check for unsupported statements first + // Lookahead is needed to disambiguate keyword from known keywords + if (!nextIs(ID_MATCH) && !nextIs(ID_LOCAL) && !nextIs(ID_INPUT)) { + parseUnsupportedStatement(status); + } else if (source[index + 1] == ID_LOCAL[1]) { parseLocalDeclaration(status); } else if (source[index + 1] == ID_INPUT[1]) { parseInputDeclaration(status); } else { - // Unsupported statement - // Lookahead is needed to disambiguate this from a `match` - if (!nextIsMatch()) { - parseUnsupportedStatement(status); - } else { - // Done parsing declarations - break; - } + // Done parsing declarations + break; } // Avoid looping infinitely @@ -1765,49 +1718,22 @@ void Parser::parseDeclarations(UErrorCode& status) { } /* - Consume an escaped curly brace, or backslash, matching the `text-escape` - nonterminal in the grammar -*/ -void Parser::parseTextEscape(UnicodeString &str, UErrorCode& status) { - parseEscapeSequence(TEXT, str, status); -} - -/* - Consume a non-empty sequence of text characters and escaped text characters, - matching the `text` nonterminal in the grammar + Consume a text character + matching the `text-char` nonterminal in the grammar - No postcondition (a message can end with a text) + No postcondition (a message can end with a text-char) */ -UnicodeString Parser::parseText(UErrorCode& status) { +UnicodeString Parser::parseTextChar(UErrorCode& status) { UnicodeString str; - if (!inBounds(source, index)) { - // Text can be empty - return str; - } - - if (!(isTextChar(source[index] || source[index] == BACKSLASH))) { - // Error -- text is expected here + if (!inBounds(source, index) || !(isTextChar(source[index]))) { + // Error -- text-char is expected here ERROR(parseError, status, index); - return str; - } - - while (true) { - if (source[index] == BACKSLASH) { - parseTextEscape(str, status); - } else if (isTextChar(source[index])) { - normalizedInput += source[index]; - str += source[index]; - index++; - maybeAdvanceLine(); - } else { - break; - } - if (!inBounds(source, index)) { - // OK for text to end a message - break; - } + } else { + normalizedInput += source[index]; + str += source[index]; + index++; + maybeAdvanceLine(); } - return str; } @@ -2026,9 +1952,22 @@ std::variant Parser::parsePlaceholder(UErrorCode& status) { return exprFallback(status); } - // Check if it's markup or an expression - if (source[index + 1] == NUMBER_SIGN || source[index + 1] == SLASH) { - // Markup + // Need to look ahead arbitrarily since can appear before the '{' and '#' + // in markup + int32_t tempIndex = index + 1; + bool isMarkup = false; + while (inBounds(source, tempIndex)) { + if (source[tempIndex] == NUMBER_SIGN || source[tempIndex] == SLASH) { + isMarkup = true; + break; + } + if (!isWhitespace(source[tempIndex])){ + break; + } + tempIndex++; + } + + if (isMarkup) { return parseMarkup(status); } return parseExpression(status); @@ -2058,9 +1997,18 @@ Pattern Parser::parseSimpleMessage(UErrorCode& status) { } break; } + case BACKSLASH: { + // Must be escaped-char + result.add(parseEscapeSequence(status), status); + break; + } + case RIGHT_CURLY_BRACE: { + // Distinguish unescaped '}' from end of quoted pattern + break; + } default: { - // Must be text - result.add(parseText(status), status); + // Must be text-char + result.add(parseTextChar(status), status); break; } } @@ -2232,21 +2180,31 @@ void Parser::parseBody(UErrorCode& status) { void Parser::parse(UParseError &parseErrorResult, UErrorCode& status) { CHECK_ERROR(status); - bool simple = true; - // Message can be empty, so we need to only look ahead - // if we know it's non-empty + bool complex = false; + // First, "look ahead" to determine if this is a simple or complex + // message. To do that, check the first non-whitespace character. + while (inBounds(source, index) && isWhitespace(source[index])) { + index++; + } if (inBounds(source, index)) { if (source[index] == PERIOD || (index < static_cast(source.length()) + 1 && source[index] == LEFT_CURLY_BRACE && source[index + 1] == LEFT_CURLY_BRACE)) { - // A complex message begins with a '.' or '{' - parseDeclarations(status); - parseBody(status); - simple = false; + complex = true; } } - if (simple) { + // Reset index + index = 0; + + // Message can be empty, so we need to only look ahead + // if we know it's non-empty + if (complex) { + parseOptionalWhitespace(status); + parseDeclarations(status); + parseBody(status); + parseOptionalWhitespace(status); + } else { // Simple message // For normalization, quote the pattern normalizedInput += LEFT_CURLY_BRACE; diff --git a/icu4c/source/i18n/messageformat2_parser.h b/icu4c/source/i18n/messageformat2_parser.h index 92c0475d67db..9367c0e981dd 100644 --- a/icu4c/source/i18n/messageformat2_parser.h +++ b/icu4c/source/i18n/messageformat2_parser.h @@ -91,10 +91,6 @@ namespace message2 { parseError.postContext[0] = '\0'; } - // Used so `parseEscapeSequence()` can handle all types of escape sequences - // (literal, text, and reserved) - typedef enum { LITERAL, TEXT, RESERVED } EscapeKind; - static void translateParseError(const MessageParseError&, UParseError&); static void setParseError(MessageParseError&, uint32_t); void maybeAdvanceLine(); @@ -111,19 +107,16 @@ namespace message2 { void parseOptionalWhitespace(UErrorCode&); void parseToken(UChar32, UErrorCode&); void parseTokenWithWhitespace(UChar32, UErrorCode&); - template - void parseToken(const UChar32 (&)[N], UErrorCode&); - template - void parseTokenWithWhitespace(const UChar32 (&)[N], UErrorCode&); - bool nextIsMatch() const; + void parseToken(const std::u16string_view&, UErrorCode&); + void parseTokenWithWhitespace(const std::u16string_view&, UErrorCode&); + bool nextIs(const std::u16string_view&) const; UnicodeString parseName(UErrorCode&); UnicodeString parseIdentifier(UErrorCode&); UnicodeString parseDigits(UErrorCode&); VariableName parseVariableName(UErrorCode&); FunctionName parseFunction(UErrorCode&); - void parseEscapeSequence(EscapeKind, UnicodeString&, UErrorCode&); - void parseLiteralEscape(UnicodeString&, UErrorCode&); - Literal parseUnquotedLiteral(UErrorCode&); + UnicodeString parseEscapeSequence(UErrorCode&); + Literal parseUnquotedLiteral(UErrorCode&); Literal parseQuotedLiteral(UErrorCode&); Literal parseLiteral(UErrorCode&); template @@ -134,7 +127,6 @@ namespace message2 { void parseOption(OptionAdder&, UErrorCode&); template void parseOptions(OptionAdder&, UErrorCode&); - void parseReservedEscape(UnicodeString&, UErrorCode&); void parseReservedChunk(Reserved::Builder&, UErrorCode&); Reserved parseReserved(UErrorCode&); Reserved parseReservedBody(Reserved::Builder&, UErrorCode&); @@ -143,8 +135,7 @@ namespace message2 { Markup parseMarkup(UErrorCode&); Expression parseExpression(UErrorCode&); std::variant parsePlaceholder(UErrorCode&); - void parseTextEscape(UnicodeString&, UErrorCode&); - UnicodeString parseText(UErrorCode&); + UnicodeString parseTextChar(UErrorCode&); Key parseKey(UErrorCode&); SelectorKeys parseNonEmptyKeys(UErrorCode&); void errorPattern(UErrorCode& status); diff --git a/icu4c/source/i18n/messageformat2_serializer.cpp b/icu4c/source/i18n/messageformat2_serializer.cpp index 23c123e6f116..2d007fa4bf5a 100644 --- a/icu4c/source/i18n/messageformat2_serializer.cpp +++ b/icu4c/source/i18n/messageformat2_serializer.cpp @@ -35,12 +35,8 @@ void Serializer::emit(const UnicodeString& s) { result += s; } -template -void Serializer::emit(const UChar32 (&token)[N]) { - // Don't emit the terminator - for (int32_t i = 0; i < N - 1; i++) { - emit(token[i]); - } +void Serializer::emit(const std::u16string_view& token) { + result.append(token); } void Serializer::emit(const Literal& l) { diff --git a/icu4c/source/i18n/messageformat2_serializer.h b/icu4c/source/i18n/messageformat2_serializer.h index 4b72d1ca715f..38d3412665a4 100644 --- a/icu4c/source/i18n/messageformat2_serializer.h +++ b/icu4c/source/i18n/messageformat2_serializer.h @@ -38,8 +38,7 @@ namespace message2 { void whitespace(); void emit(UChar32); - template - void emit(const UChar32 (&)[N]); + void emit(const std::u16string_view&); void emit(const UnicodeString&); void emit(const Literal&); void emit(const Key&); diff --git a/icu4c/source/i18n/unicode/messageformat2_data_model.h b/icu4c/source/i18n/unicode/messageformat2_data_model.h index 942a03f73591..65dc836caeb3 100644 --- a/icu4c/source/i18n/unicode/messageformat2_data_model.h +++ b/icu4c/source/i18n/unicode/messageformat2_data_model.h @@ -2599,7 +2599,7 @@ namespace message2 { // If non-null, the referent is a member of `expr` so // its lifetime is the same as the lifetime of the enclosing Binding // (as long as there's no mutation) - const Callable* annotation = nullptr; + const std::variant* annotation = nullptr; const OptionMap& getOptionsInternal() const; diff --git a/icu4c/source/test/intltest/messageformat2test_read_json.cpp b/icu4c/source/test/intltest/messageformat2test_read_json.cpp index 33e65a92ce85..0c65764e7fb5 100644 --- a/icu4c/source/test/intltest/messageformat2test_read_json.cpp +++ b/icu4c/source/test/intltest/messageformat2test_read_json.cpp @@ -18,52 +18,44 @@ using namespace nlohmann; using namespace icu::message2; -static UErrorCode getExpectedErrorFromString(const std::string& errorName) { - if (errorName == "Variant Key Mismatch") { +static UErrorCode getExpectedRuntimeErrorFromString(const std::string& errorName) { + if (errorName == "syntax-error") { + return U_MF_SYNTAX_ERROR; + } + if (errorName == "variant-key-mismatch") { return U_MF_VARIANT_KEY_MISMATCH_ERROR; } - if (errorName == "Missing Fallback Variant") { + if (errorName == "missing-fallback-variant") { return U_MF_NONEXHAUSTIVE_PATTERN_ERROR; } - if (errorName == "Missing Selector Annotation") { + if (errorName == "missing-selector-annotation") { return U_MF_MISSING_SELECTOR_ANNOTATION_ERROR; } - if (errorName == "Duplicate Declaration") { - return U_MF_DUPLICATE_DECLARATION_ERROR; - } - if (errorName == "Unsupported Statement") { - return U_MF_UNSUPPORTED_STATEMENT_ERROR; - } -// Arbitrary default - return U_MF_DUPLICATE_OPTION_NAME_ERROR; -} - -static UErrorCode getExpectedRuntimeErrorFromString(const std::string& errorName) { - if (errorName == "parse-error" || errorName == "empty-token" || errorName == "extra-content") { - return U_MF_SYNTAX_ERROR; - } - if (errorName == "key-mismatch") { - return U_MF_VARIANT_KEY_MISMATCH_ERROR; - } - if (errorName == "missing-var" || errorName == "unresolved-var") { + if (errorName == "unresolved-variable") { return U_MF_UNRESOLVED_VARIABLE_ERROR; } - if (errorName == "unsupported-annotation") { + if (errorName == "unsupported-expression") { return U_MF_UNSUPPORTED_EXPRESSION_ERROR; } - if (errorName == "bad-input" || errorName == "RangeError") { + if (errorName == "bad-operand") { return U_MF_OPERAND_MISMATCH_ERROR; } if (errorName == "bad-option") { return U_MF_FORMATTING_ERROR; } - if (errorName == "missing-func") { + if (errorName == "unknown-function") { return U_MF_UNKNOWN_FUNCTION_ERROR; } if (errorName == "duplicate-declaration") { return U_MF_DUPLICATE_DECLARATION_ERROR; } - if (errorName == "selector-error") { + if (errorName == "duplicate-option-name") { + return U_MF_DUPLICATE_OPTION_NAME_ERROR; + } + if (errorName == "duplicate-variant") { + return U_MF_DUPLICATE_VARIANT_ERROR; + } + if (errorName == "bad-selector") { return U_MF_SELECTOR_ERROR; } if (errorName == "formatting-error") { @@ -88,56 +80,86 @@ static void makeTestName(char* buffer, size_t size, std::string fileName, int32_ snprintf(buffer, size, "test from file: %s[%u]", fileName.c_str(), ++testNum); } -static bool setArguments(TestCase::Builder& test, const json::object_t& params, UErrorCode& errorCode) { +static bool setArguments(TestMessageFormat2& t, + TestCase::Builder& test, + const std::vector& params, + UErrorCode& errorCode) { if (U_FAILURE(errorCode)) { return true; } + bool schemaError = false; for (auto argsIter = params.begin(); argsIter != params.end(); ++argsIter) { - const UnicodeString argName = u_str(argsIter->first); - // Determine type of value - if (argsIter->second.is_number()) { - test.setArgument(argName, - argsIter->second.template get()); - } else if (argsIter->second.is_string()) { - test.setArgument(argName, - u_str(argsIter->second.template get())); - } else if (argsIter->second.is_object()) { - // Dates: represent in tests as { "date" : timestamp }, to distinguish - // from number values - auto obj = argsIter->second.template get(); - if (obj["date"].is_number()) { - test.setDateArgument(argName, obj["date"]); - } else if (obj["decimal"].is_string()) { - // Decimal strings: represent in tests as { "decimal" : string }, - // to distinguish from string values - test.setDecimalArgument(argName, obj["decimal"].template get(), errorCode); + auto j_object = argsIter->template get(); + if (!j_object["name"].is_null()) { + const UnicodeString argName = u_str(j_object["name"].template get()); + if (!j_object["value"].is_null()) { + json val = j_object["value"]; + // Determine type of value + if (val.is_number()) { + test.setArgument(argName, + val.template get()); + } else if (val.is_string()) { + test.setArgument(argName, + u_str(val.template get())); + } else if (val.is_object()) { + // Dates: represent in tests as { "date" : timestamp }, to distinguish + // from number values + auto obj = val.template get(); + if (obj["date"].is_number()) { + test.setDateArgument(argName, val["date"]); + } else if (obj["decimal"].is_string()) { + // Decimal strings: represent in tests as { "decimal" : string }, + // to distinguish from string values + test.setDecimalArgument(argName, obj["decimal"].template get(), errorCode); + } + } else if (val.is_boolean() || val.is_null()) { + return false; // For now, boolean and null arguments are unsupported + } + } else { + schemaError = true; + break; } - } else if (argsIter->second.is_boolean() || argsIter->second.is_null()) { - return false; // For now, boolean and null arguments are unsupported + } else { + schemaError = true; + break; + } + } + if (schemaError) { + t.logln("Warning: test with missing 'name' or 'value' in params"); + if (U_SUCCESS(errorCode)) { + errorCode = U_ILLEGAL_ARGUMENT_ERROR; } } return true; } + +/* + Test files are expected to follow the schema in: + https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + as of https://github.com/unicode-org/conformance/pull/255 +*/ static void runValidTest(TestMessageFormat2& icuTest, const std::string& testName, + const std::string& defaultError, const json& j, IcuTestErrorCode& errorCode) { auto j_object = j.template get(); std::string messageText; + // src can be a single string or an array of strings if (!j_object["src"].is_null()) { - messageText = j_object["src"].template get(); - } else { - if (!j_object["srcs"].is_null()) { - auto strings = j_object["srcs"].template get>(); + if (j_object["src"].is_string()) { + messageText = j_object["src"].template get(); + } else { + auto strings = j_object["src"].template get>(); for (const auto &piece : strings) { messageText += piece; } } - // Otherwise, it should probably be an error, but we just - // treat this as the empty string } + // Otherwise, it should probably be an error, but we just + // treat this as the empty string TestCase::Builder test = successTest(testName, messageText); @@ -160,16 +182,17 @@ static void runValidTest(TestMessageFormat2& icuTest, } if (!j_object["params"].is_null()) { - // Map from string to json - auto params = j_object["params"].template get(); - if (!setArguments(test, params, errorCode)) { + // `params` is an array of objects + auto params = j_object["params"].template get>(); + if (!setArguments(icuTest, test, params, errorCode)) { return; // Skip tests with unsupported arguments } } - if (!j_object["errors"].is_null()) { + bool expectedError = false; + if (!j_object["expErrors"].is_null()) { // Map from string to string - auto errors = j_object["errors"].template get>>(); + auto errors = j_object["expErrors"].template get>>(); // We only emit the first error, so we just hope the first error // in the list in the test is also the error we emit U_ASSERT(errors.size() > 0); @@ -182,22 +205,18 @@ static void runValidTest(TestMessageFormat2& icuTest, return; } test.setExpectedError(getExpectedRuntimeErrorFromString(errorType)); + expectedError = true; + } else if (defaultError.length() > 0) { + test.setExpectedError(getExpectedRuntimeErrorFromString(defaultError)); + expectedError = true; } - TestCase t = test.build(); - TestUtils::runTestCase(icuTest, t, errorCode); -} - -static void runSyntaxErrorTest(TestMessageFormat2& icuTest, - const std::string& testName, - const json& j, - IcuTestErrorCode& errorCode) { - auto messageText = j["src"].template get(); - TestCase::Builder test = successTest(testName, messageText) - .setExpectedError(U_MF_SYNTAX_ERROR); - - auto j_object = j.template get(); + // If no expected result and no error, then set the test builder to expect success + if (j_object["exp"].is_null() && !expectedError) { + test.setNoSyntaxError(); + } + // Check for expected diagnostic values int32_t lineNumber = 0; int32_t offset = -1; if (!j_object["char"].is_null()) { @@ -209,117 +228,14 @@ static void runSyntaxErrorTest(TestMessageFormat2& icuTest, if (offset != -1) { test.setExpectedLineNumberAndOffset(lineNumber, offset); } - TestCase t = test.build(); - TestUtils::runTestCase(icuTest, t, errorCode); -} - -// File name is relative to message2/ in the test data directory -static void runICU4JSyntaxTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string testFileName(testDataDirectory); - testFileName.append("message2/"); - testFileName.append(fileName); - std::ifstream testFile(testFileName); - json data = json::parse(testFile); - - // Map from string to json, where the strings are the function names - auto tests = data.template get(); - for (auto iter = tests.begin(); iter != tests.end(); ++iter) { - int32_t testNum = 0; - auto categoryName = iter->first; - t.logln("ICU4J syntax tests:"); - t.logln(u_str(iter->second.dump())); - - // Array of tests - auto testsForThisCategory = iter->second.template get>(); - - TestCase::Builder test; - test.setNoSyntaxError(); - for (auto testsIter = testsForThisCategory.begin(); - testsIter != testsForThisCategory.end(); - ++testsIter) { - char testName[100]; - makeTestName(testName, sizeof(testName), categoryName, ++testNum); - t.logln(testName); - - // Tests here are just strings, and we test that they run without syntax errors - test.setPattern(u_str(*testsIter)); - TestCase testCase = test.build(); - TestUtils::runTestCase(t, testCase, errorCode); - } - - } -} - -// File name is relative to message2/ in the test data directory -static void runICU4JSelectionTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string testFileName(testDataDirectory); - testFileName.append("message2/"); - testFileName.append(fileName); - std::ifstream testFile(testFileName); - json data = json::parse(testFile); - - int32_t testNum = 0; - for (auto iter = data.begin(); iter != data.end(); ++iter) { - // Each test has a "shared" and a "variations" field - auto j_object = iter->get(); - auto shared = j_object["shared"]; - auto variations = j_object["variations"]; - // Skip ignored tests - if (!j_object["ignoreCpp"].is_null()) { - return; - } - - // shared has a "srcs" field - auto strings = shared["srcs"].template get>(); - std::string messageText; - for (const auto &piece : strings) { - messageText += piece; - } - - t.logln(u_str("ICU4J selectors tests: " + fileName)); - t.logln(u_str(iter->dump())); - - TestCase::Builder test; - char testName[100]; - makeTestName(testName, sizeof(testName), fileName, ++testNum); - test.setName(testName); - - // variations has "params" and "exp" fields, and an optional "locale" - for (auto variationsIter = variations.begin(); variationsIter != variations.end(); ++variationsIter) { - auto variation = variationsIter->get(); - auto params = variation["params"]; - auto exp = variation["exp"]; - - test.setExpected(u_str(exp)); - test.setPattern(u_str(messageText)); - test.setExpectSuccess(); - setArguments(test, params, errorCode); - - if (!variation["locale"].is_null()) { - std::string localeStr = variation["locale"].template get(); - test.setLocale(Locale(localeStr.c_str())); - } - - TestCase testCase = test.build(); - TestUtils::runTestCase(t, testCase, errorCode); - } - } + TestCase t = test.build(); + TestUtils::runTestCase(icuTest, t, errorCode); } // File name is relative to message2/ in the test data directory -static void runValidTestsFromJsonFile(TestMessageFormat2& t, +static void runTestsFromJsonFile(TestMessageFormat2& t, const std::string& fileName, IcuTestErrorCode& errorCode) { const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); @@ -334,156 +250,41 @@ static void runValidTestsFromJsonFile(TestMessageFormat2& t, int32_t testNum = 0; char testName[100]; - for (auto iter = data.begin(); iter != data.end(); ++iter) { - makeTestName(testName, sizeof(testName), fileName, ++testNum); - t.logln(testName); - - t.logln(u_str(iter->dump())); - - runValidTest(t, testName, *iter, errorCode); - } -} - -// File name is relative to message2/ in the test data directory -static void runDataModelErrorTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string dataModelErrorsFileName(testDataDirectory); - dataModelErrorsFileName.append("message2/"); - dataModelErrorsFileName.append(fileName); - std::ifstream dataModelErrorsFile(dataModelErrorsFileName); - json data = json::parse(dataModelErrorsFile); - - // Do tests for data model errors - // This file is an object where the keys are error names - // and the values are arrays of strings. - // The whole file can be represented - // as a map from strings to a vector of strings. - using dataModelErrorType = std::map>; - auto dataModelErrorTests = data.template get(); - for (auto iter = dataModelErrorTests.begin(); iter != dataModelErrorTests.end(); ++iter) { - auto errorName = iter->first; - auto messages = iter->second; - - UErrorCode expectedError = getExpectedErrorFromString(errorName); - int32_t testNum = 0; - char testName[100]; - TestCase::Builder testBuilder; - for (auto messagesIter = messages.begin(); messagesIter != messages.end(); ++messagesIter) { - makeTestName(testName, sizeof(testName), errorName, testNum); - testBuilder.setName(testName); - t.logln(u_str(fileName + ": " + testName)); - testNum++; - UnicodeString messageText = u_str(*messagesIter); - t.logln(messageText); - - TestCase test = testBuilder.setPattern(messageText) - .setExpectedError(expectedError) - .build(); - TestUtils::runTestCase(t, test, errorCode); + auto j_object = data.template get(); + + // Some files have an expected error + std::string defaultError; + if (!j_object["defaultTestProperties"].is_null() + && !j_object["defaultTestProperties"]["expErrors"].is_null()) { + auto expErrors = j_object["defaultTestProperties"]["expErrors"]; + // expErrors might also be a boolean, in which case we ignore it -- + // so we have to check if it's an array + if (expErrors.is_array()) { + auto expErrorsObj = expErrors.template get>(); + if (expErrorsObj.size() > 0) { + if (!expErrorsObj[0]["type"].is_null()) { + defaultError = expErrorsObj[0]["type"].template get(); + } + } } } -} - -// File name is relative to message2/ in the test data directory -static void runSyntaxErrorTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string syntaxErrorsFileName(testDataDirectory); - syntaxErrorsFileName.append("message2/"); - syntaxErrorsFileName.append(fileName); - std::ifstream syntaxErrorsFile(syntaxErrorsFileName); - json data = json::parse(syntaxErrorsFile); - - // Do tests for syntax errors - // This file is just an array of strings - int32_t testNum = 0; - char testName[100]; - TestCase::Builder testBuilder; - for (auto iter = data.begin(); iter != data.end(); ++iter) { - makeTestName(testName, sizeof(testName), fileName, ++testNum); - testBuilder.setName(testName); - t.logln(testName); - - json json_string = *iter; - UnicodeString cpp_string = u_str(json_string.template get()); - - t.logln(cpp_string); - TestCase test = testBuilder.setPattern(cpp_string) - .setExpectedError(U_MF_SYNTAX_ERROR) - .build(); - TestUtils::runTestCase(t, test, errorCode); - } -} - -// File name is relative to message2/ in the test data directory -static void runSyntaxTestsWithDiagnosticsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string testFileName(testDataDirectory); - testFileName.append("message2/"); - testFileName.append(fileName); - std::ifstream testFile(testFileName); - json data = json::parse(testFile); - - int32_t testNum = 0; - char testName[100]; - - for (auto iter = data.begin(); iter != data.end(); ++iter) { - makeTestName(testName, sizeof(testName), fileName, ++testNum); - t.logln(testName); - t.logln(u_str(iter->dump())); - - runSyntaxErrorTest(t, testName, *iter, errorCode); - } -} - -// File name is relative to message2/ in the test data directory -static void runFunctionTestsFromJsonFile(TestMessageFormat2& t, - const std::string& fileName, - IcuTestErrorCode& errorCode) { - // Get the test data directory - const char* testDataDirectory = IntlTest::getSharedTestData(errorCode); - CHECK_ERROR(errorCode); - - std::string functionTestsFileName(testDataDirectory); - functionTestsFileName.append("message2/"); - functionTestsFileName.append(fileName); - std::ifstream functionTestsFile(functionTestsFileName); - json data = json::parse(functionTestsFile); - - // Map from string to json, where the strings are the function names - auto tests = data.template get(); - for (auto iter = tests.begin(); iter != tests.end(); ++iter) { - int32_t testNum = 0; - auto functionName = iter->first; - t.logln(u_str("Function tests: " + fileName)); - t.logln(u_str(iter->second.dump())); - - // Array of tests - auto testsForThisFunction = iter->second.template get>(); - for (auto testsIter = testsForThisFunction.begin(); - testsIter != testsForThisFunction.end(); - ++testsIter) { - char testName[100]; - makeTestName(testName, sizeof(testName), functionName, ++testNum); + if (!j_object["tests"].is_null()) { + auto tests = j_object["tests"].template get>(); + for (auto iter = tests.begin(); iter != tests.end(); ++iter) { + makeTestName(testName, sizeof(testName), fileName, ++testNum); t.logln(testName); - runValidTest(t, testName, *testsIter, errorCode); - } + t.logln(u_str(iter->dump())); + runValidTest(t, testName, defaultError, *iter, errorCode); + } + } else { + // Test doesn't follow schema -- probably an error + t.logln("Warning: no tests in filename: "); + t.logln(u_str(fileName)); + (UErrorCode&) errorCode = U_ILLEGAL_ARGUMENT_ERROR; } - } void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { @@ -493,27 +294,34 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { // Tests directly under testdata/message2 are specific to ICU4C. // Do spec tests for syntax errors - runSyntaxErrorTestsFromJsonFile(*this, "spec/syntax-errors.json", errorCode); - runSyntaxErrorTestsFromJsonFile(*this, "more-syntax-errors.json", errorCode); + runTestsFromJsonFile(*this, "spec/syntax-errors.json", errorCode); // Do tests for data model errors - runDataModelErrorTestsFromJsonFile(*this, "spec/data-model-errors.json", errorCode); - runDataModelErrorTestsFromJsonFile(*this, "more-data-model-errors.json", errorCode); + runTestsFromJsonFile(*this, "spec/data-model-errors.json", errorCode); + runTestsFromJsonFile(*this, "more-data-model-errors.json", errorCode); + + // Do tests for reserved statements/expressions + runTestsFromJsonFile(*this, "spec/unsupported-expressions.json", errorCode); + runTestsFromJsonFile(*this, "spec/unsupported-statements.json", errorCode); // Do valid spec tests - runValidTestsFromJsonFile(*this, "spec/test-core.json", errorCode); + runTestsFromJsonFile(*this, "spec/syntax.json", errorCode); // Do valid function tests - runFunctionTestsFromJsonFile(*this, "spec/test-functions.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/date.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/datetime.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/integer.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/number.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/string.json", errorCode); + runTestsFromJsonFile(*this, "spec/functions/time.json", errorCode); // Other tests (non-spec) - runFunctionTestsFromJsonFile(*this, "more-functions.json", errorCode); - runValidTestsFromJsonFile(*this, "valid-tests.json", errorCode); - runValidTestsFromJsonFile(*this, "resolution-errors.json", errorCode); - runValidTestsFromJsonFile(*this, "reserved-syntax.json", errorCode); - runValidTestsFromJsonFile(*this, "matches-whitespace.json", errorCode); - runValidTestsFromJsonFile(*this, "alias-selector-annotations.json", errorCode); - runValidTestsFromJsonFile(*this, "runtime-errors.json", errorCode); + runTestsFromJsonFile(*this, "more-functions.json", errorCode); + runTestsFromJsonFile(*this, "valid-tests.json", errorCode); + runTestsFromJsonFile(*this, "resolution-errors.json", errorCode); + runTestsFromJsonFile(*this, "matches-whitespace.json", errorCode); + runTestsFromJsonFile(*this, "alias-selector-annotations.json", errorCode); + runTestsFromJsonFile(*this, "runtime-errors.json", errorCode); // Re: the expected output for the first test in this file: // Note: the more "correct" fallback output seems like it should be "1.000 3" (ignoring the @@ -523,10 +331,10 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { // Probably this is going to change anyway so that any data model error gets replaced // with a fallback for the whole message. // The second test has a similar issue with the output. - runValidTestsFromJsonFile(*this, "tricky-declarations.json", errorCode); + runTestsFromJsonFile(*this, "tricky-declarations.json", errorCode); // Markup is ignored when formatting to string - runValidTestsFromJsonFile(*this, "markup.json", errorCode); + runTestsFromJsonFile(*this, "markup.json", errorCode); // TODO(duplicates): currently the expected output is based on using // the last definition of the duplicate-declared variable; @@ -534,24 +342,24 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { // however if https://github.com/unicode-org/message-format-wg/pull/704 lands, // it'll be a moot point since the output will be expected to be the fallback string // (This applies to the expected output for all the U_DUPLICATE_DECLARATION_ERROR tests) - runValidTestsFromJsonFile(*this, "duplicate-declarations.json", errorCode); + runTestsFromJsonFile(*this, "duplicate-declarations.json", errorCode); // TODO(options): // Bad options. The spec is unclear about this // -- see https://github.com/unicode-org/message-format-wg/issues/738 // The current behavior is to set a U_MF_FORMATTING_ERROR for any invalid options. - runValidTestsFromJsonFile(*this, "invalid-options.json", errorCode); + runTestsFromJsonFile(*this, "invalid-options.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-end-of-input.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-diagnostics.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "invalid-number-literals-diagnostics.json", errorCode); - runSyntaxTestsWithDiagnosticsFromJsonFile(*this, "syntax-errors-diagnostics-multiline.json", errorCode); + runTestsFromJsonFile(*this, "syntax-errors-end-of-input.json", errorCode); + runTestsFromJsonFile(*this, "syntax-errors-diagnostics.json", errorCode); + runTestsFromJsonFile(*this, "invalid-number-literals-diagnostics.json", errorCode); + runTestsFromJsonFile(*this, "syntax-errors-diagnostics-multiline.json", errorCode); // ICU4J tests - runFunctionTestsFromJsonFile(*this, "icu-test-functions.json", errorCode); - runICU4JSyntaxTestsFromJsonFile(*this, "icu-parser-tests.json", errorCode); - runICU4JSelectionTestsFromJsonFile(*this, "icu-test-selectors.json", errorCode); - runValidTestsFromJsonFile(*this, "icu-test-previous-release.json", errorCode); + runTestsFromJsonFile(*this, "icu-test-functions.json", errorCode); + runTestsFromJsonFile(*this, "icu-parser-tests.json", errorCode); + runTestsFromJsonFile(*this, "icu-test-selectors.json", errorCode); + runTestsFromJsonFile(*this, "icu-test-previous-release.json", errorCode); } #endif /* #if !UCONFIG_NO_MF2 */ diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java index e33a7c9804a8..2ba70854d093 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFDataModelFormatter.java @@ -436,10 +436,8 @@ private static Object resolveLiteralOrVariable( Map arguments) { if (value instanceof Literal) { String val = ((Literal) value).value; - Number nr = OptUtils.asNumber(val); - if (nr != null) { - return nr; - } + // "The resolution of a text or literal MUST resolve to a string." + // https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#literal-resolution return val; } else if (value instanceof VariableRef) { String varName = ((VariableRef) value).name; diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java index 073aafd8ef32..a5c826f87397 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/message2/MFParser.java @@ -46,22 +46,32 @@ public static MFDataModel.Message parse(String input) throws MFParseException { // Parser proper private MFDataModel.Message parseImpl() throws MFParseException { MFDataModel.Message result; + // Determine if message is simple or complex; this requires + // looking through whitespace. + int savedPosition = input.getPosition(); + skipOptionalWhitespaces(); int cp = input.peekChar(); if (cp == '.') { // declarations or .match + // No need to restore whitespace result = getComplexMessage(); } else if (cp == '{') { // `{` or `{{` cp = input.readCodePoint(); cp = input.peekChar(); if (cp == '{') { // `{{`, complex body without declarations input.backup(1); // let complexBody deal with the wrapping {{ and }} + // abnf: complex-message = [s] *(declaration [s]) complex-body [s] MFDataModel.Pattern pattern = getQuotedPattern(); + skipOptionalWhitespaces(); result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern); } else { // placeholder - input.backup(1); // We want the '{' present, to detect the part as placeholder. + // Restore whitespace if applicable + input.gotoPosition(savedPosition); MFDataModel.Pattern pattern = getPattern(); result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern); } } else { + // Restore whitespace if applicable + input.gotoPosition(savedPosition); MFDataModel.Pattern pattern = getPattern(); result = new MFDataModel.PatternMessage(new ArrayList<>(), pattern); } @@ -586,9 +596,10 @@ private MFDataModel.Message getComplexMessage() throws MFParseException { } else { // Expect {{...}} or end of message skipOptionalWhitespaces(); int cp = input.peekChar(); - // complex-message = *(declaration [s]) complex-body + // abnf: complex-message = [s] *(declaration [s]) complex-body [s] checkCondition(cp != EOF, "Expected a quoted pattern or .match; got end-of-input"); MFDataModel.Pattern pattern = getQuotedPattern(); + skipOptionalWhitespaces(); // Trailing whitespace is allowed checkCondition(input.atEnd(), "Content detected after the end of the message."); return new MFDataModel.PatternMessage(declarations, pattern); } @@ -648,9 +659,7 @@ private MFDataModel.Variant getVariant() throws MFParseException { } keys.add(key); } - // Only want to skip whitespace if we parsed at least one key -- - // otherwise, we might fail to catch trailing whitespace at the end of - // the message, which is a parse error + // Only want to skip whitespace if we parsed at least one key if (!keys.isEmpty()) { skipOptionalWhitespaces(); } @@ -712,7 +721,7 @@ private MFDataModel.Declaration getDeclaration() throws MFParseException { MFDataModel.Expression expression; switch (declName) { case "input": - skipMandatoryWhitespaces(); + skipOptionalWhitespaces(); expression = getPlaceholder(); String inputVarName = null; checkCondition(expression instanceof MFDataModel.VariableExpression, diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java index 3305f2435a50..0dd3bf6be59c 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/CoreTest.java @@ -4,11 +4,14 @@ package com.ibm.icu.dev.test.message2; import java.io.Reader; +import java.lang.reflect.Type; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import com.google.gson.reflect.TypeToken; import com.ibm.icu.dev.test.CoreTestFmwk; @SuppressWarnings({"static-method", "javadoc"}) @@ -16,14 +19,30 @@ public class CoreTest extends CoreTestFmwk { private static final String[] JSON_FILES = {"alias-selector-annotations.json", "duplicate-declarations.json", + "icu-parser-tests.json", + "icu-test-functions.json", + "icu-test-previous-release.json", + "icu-test-selectors.json", + "invalid-number-literals-diagnostics.json", "invalid-options.json", "markup.json", "matches-whitespace.json", - "reserved-syntax.json", + "more-data-model-errors.json", + "more-functions.json", "resolution-errors.json", "runtime-errors.json", - "spec/test-core.json", + "spec/data-model-errors.json", + "spec/syntax-errors.json", + "spec/syntax.json", + "spec/functions/date.json", + "spec/functions/datetime.json", + "spec/functions/integer.json", + "spec/functions/number.json", + "spec/functions/string.json", + "spec/functions/time.json", "syntax-errors-diagnostics.json", + "syntax-errors-diagnostics-multiline.json", + "syntax-errors-end-of-input.json", "tricky-declarations.json", "valid-tests.json"}; @@ -31,9 +50,9 @@ public class CoreTest extends CoreTestFmwk { public void test() throws Exception { for (String jsonFile : JSON_FILES) { try (Reader reader = TestUtils.jsonReader(jsonFile)) { - Unit[] unitList = TestUtils.GSON.fromJson(reader, Unit[].class); - for (Unit unit : unitList) { - TestUtils.runTestCase(unit); + MF2Test tests = TestUtils.GSON.fromJson(reader, MF2Test.class); + for (Unit unit : tests.tests) { + TestUtils.runTestCase(tests.defaultTestProperties, unit); } } } diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java deleted file mode 100644 index d8ba5699c260..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DataModelErrorsTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.google.gson.reflect.TypeToken; -import com.ibm.icu.dev.test.CoreTestFmwk; -import com.ibm.icu.message2.MessageFormatter; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class DataModelErrorsTest extends CoreTestFmwk { - private static final String[] JSON_FILES = {"spec/data-model-errors.json", - "more-data-model-errors.json"}; - - @Test - public void test() throws Exception { - for (String jsonFile : JSON_FILES) { - try (Reader reader = TestUtils.jsonReader(jsonFile)) { - Type mapType = new TypeToken>(){/* not code */}.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry tests : unitList.entrySet()) { - for (String pattern : tests.getValue()) { - try { - MessageFormatter.builder().setPattern(pattern).build().formatToString(null); - fail("Undetected errors in '" + tests.getKey() + "': '" + pattern + "'"); - } catch (Exception e) { - // We expected an error, so it's all good - } - } - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java new file mode 100644 index 000000000000..d0b463baf1f3 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/DefaultTestProperties.java @@ -0,0 +1,23 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + +// Class corresponding to the json test files. +// Since this is serialized by Gson, the field names should match the keys in the .json files. +class DefaultTestProperties { + // Unused fields ignored + final String locale; + final Object[] expErrors; + + DefaultTestProperties(String locale, Object[] expErrors) { + this.locale = locale; + this.expErrors = expErrors; + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java deleted file mode 100644 index 994366a559aa..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FirstReleaseTests.java +++ /dev/null @@ -1,33 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.ibm.icu.dev.test.CoreTestFmwk; - -/* - * This is the equivalent of the `FromJsonTest` class in the previous release. - * That class was originally a json file, converted to some hard-coded tests in the Java class. - * Now that we can use gson for testing we reverted those tests back to json, tested in this class. - */ -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class FirstReleaseTests extends CoreTestFmwk { - private static final String JSON_FILE = "icu-test-previous-release.json"; - - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - Unit[] unitList = TestUtils.GSON.fromJson(reader, Unit[].class); - for (Unit unit : unitList) { - TestUtils.runTestCase(unit); - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java deleted file mode 100644 index c76c60a50327..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/FunctionsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.google.gson.reflect.TypeToken; -import com.ibm.icu.dev.test.CoreTestFmwk; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class FunctionsTest extends CoreTestFmwk { - private static final String[] JSON_FILES = {"spec/test-functions.json", - "more-functions.json"}; - - @Test - public void test() throws Exception { - for (String jsonFile : JSON_FILES) { - try (Reader reader = TestUtils.jsonReader(jsonFile)) { - Type mapType = new TypeToken>(){/* not code */}.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry testGroup : unitList.entrySet()) { - for (Unit unit : testGroup.getValue()) { - TestUtils.runTestCase(unit); - } - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java deleted file mode 100644 index f286d5c2c743..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/IcuFunctionsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.Map.Entry; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.google.gson.reflect.TypeToken; -import com.ibm.icu.dev.test.CoreTestFmwk; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class IcuFunctionsTest extends CoreTestFmwk { - private static final String JSON_FILE = "icu-test-functions.json"; - - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - Type mapType = - new TypeToken>() { - /* not code */ - }.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry testGroup : unitList.entrySet()) { - for (Unit unit : testGroup.getValue()) { - TestUtils.runTestCase(unit); - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java new file mode 100644 index 000000000000..502fde2a6093 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/MF2Test.java @@ -0,0 +1,24 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + +// Class corresponding to the json test files. +// Since this is serialized by Gson, the field names should match the keys in the .json files. +class MF2Test { + // Unused fields ignored + final DefaultTestProperties defaultTestProperties; + final Unit[] tests; + + MF2Test(DefaultTestProperties defaultTestProperties, + Unit[] tests) { + this.defaultTestProperties = defaultTestProperties; + this.tests = tests; + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java new file mode 100644 index 000000000000..9c77fe73443b --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Param.java @@ -0,0 +1,23 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// See https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json + +// Class corresponding to the json test files. +// Since this is serialized by Gson, the field names should match the keys in the .json files. +class Param { + // Unused fields ignored + final String name; + final Object value; + + Param(String name, Object value) { + this.name = name; + this.value = value; + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java index 7b2a3b988c9e..638b38521b27 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/ParserSmokeTest.java @@ -29,16 +29,5 @@ public void testNullInput() throws Exception { MFParser.parse(null); } - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - Type mapType = new TypeToken>(){/* not code */}.getType(); - Map unitList = TestUtils.GSON.fromJson(reader, mapType); - for (Entry testGroup : unitList.entrySet()) { - for (String unit : testGroup.getValue()) { - MFParser.parse(unit); - } - } - } - } + // Other tests in CoreTest.java } diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java deleted file mode 100644 index 8762f4757259..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SelectorsWithVariousArgumentsTest.java +++ /dev/null @@ -1,39 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.ibm.icu.dev.test.CoreTestFmwk; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class SelectorsWithVariousArgumentsTest extends CoreTestFmwk { - private static final String JSON_FILE = "icu-test-selectors.json"; - - @Test - public void test() throws Exception { - try (Reader reader = TestUtils.jsonReader(JSON_FILE)) { - TestWithVariations[] unitList = - TestUtils.GSON.fromJson(reader, TestWithVariations[].class); - for (TestWithVariations testWithVar : unitList) { - Unit sharedUnit = testWithVar.shared; - for (Unit variation : testWithVar.variations) { - Unit mergedUnit = sharedUnit.merge(variation); - TestUtils.runTestCase(mergedUnit); - } - } - } - } - - class TestWithVariations { - String comment; - Unit shared; - Unit[] variations; - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java new file mode 100644 index 000000000000..39a931a5a589 --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Sources.java @@ -0,0 +1,26 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +// Class corresponding to the json test files. +// See Unit.java and StringToListAdapter.java for how this is used. +// Workaround for not being able to get the class of a generic type. + +class Sources { + final List sources; + + Sources(List sources) { + this.sources = sources; + } + + @Override + public String toString() { + return ("[" + sources.toString() + "]"); + } +} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java new file mode 100644 index 000000000000..facc9f00c2df --- /dev/null +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/StringToListAdapter.java @@ -0,0 +1,52 @@ +// © 2024 and later: Unicode, Inc. and others. +// License & terms of use: https://www.unicode.org/copyright.html + +package com.ibm.icu.dev.test.message2; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.JsonToken; + +// Helper class that converts a single String to a List +// so that the `src` property can be either a single string or an array of strings. +// Used in the TestUtils class. + +// Uses ArrayList instead of List so that when registering, it's possible +// to get ArrayList.class +public class StringToListAdapter extends TypeAdapter { + public Sources read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + if (reader.peek() == JsonToken.BEGIN_ARRAY) { + ArrayList result = new ArrayList(); + reader.beginArray(); + while (reader.hasNext()) { + result.add(reader.nextString()); + } + reader.endArray(); + return new Sources(result); + } + if (reader.peek() == JsonToken.STRING) { + String str = reader.nextString(); + ArrayList result = new ArrayList(); + result.add(str); + return new Sources(result); + } + throw new IOException(); + } + public void write(JsonWriter writer, Sources value) throws IOException { + writer.beginArray(); + for (String s : value.sources) { + writer.value(s); + } + writer.endArray(); + } +} + diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java deleted file mode 100644 index 204f79555161..000000000000 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/SyntaxErrorsTest.java +++ /dev/null @@ -1,37 +0,0 @@ -// © 2024 and later: Unicode, Inc. and others. -// License & terms of use: https://www.unicode.org/copyright.html - -package com.ibm.icu.dev.test.message2; - -import java.io.Reader; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import com.ibm.icu.dev.test.CoreTestFmwk; -import com.ibm.icu.message2.MessageFormatter; - -@SuppressWarnings({"static-method", "javadoc"}) -@RunWith(JUnit4.class) -public class SyntaxErrorsTest extends CoreTestFmwk { - private static final String[] JSON_FILES = {"more-syntax-errors.json", - "spec/syntax-errors.json"}; - - @Test - public void test() throws Exception { - for (String jsonFile : JSON_FILES) { - try (Reader reader = TestUtils.jsonReader(jsonFile)) { - String[] srcList = TestUtils.GSON.fromJson(reader, String[].class); - for (String source : srcList) { - try { - MessageFormatter.builder().setPattern(source).build(); - fail("Pattern expected to fail, but didn't: '" + source + "'"); - } catch (Exception e) { - // If we get here it is fine - } - } - } - } - } -} diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java index 572da833e67a..4a276216462c 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/TestUtils.java @@ -17,6 +17,7 @@ import java.util.Date; import java.util.Locale; import java.util.Map; +import java.util.TreeMap; import org.junit.Ignore; @@ -28,7 +29,11 @@ /** Utility class, has no test methods. */ @Ignore("Utility class, has no test methods.") public class TestUtils { - static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + + static final Gson GSON = new GsonBuilder() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .registerTypeAdapter(Sources.class, new StringToListAdapter()) + .create(); // ======= Legacy TestCase utilities, no json-compatible ======== @@ -72,62 +77,73 @@ private static String reportCase(TestCase testCase) { // ======= Same functionality with Unit, usable with JSON ======== - static void rewriteDates(Map params) { + static void rewriteDates(Param[] params) { // For each value in `params` that's a map with the single key // `date` and a double value d, // return a map with that value changed to Date(d) // In JSON this looks like: - // "params": {"exp": { "date": 1722746637000 } } - for (Map.Entry pair : params.entrySet()) { - if (pair.getValue() instanceof Map) { - Map innerMap = (Map) pair.getValue(); + // "params": [{"name": "exp"}, { "value": { "date": 1722746637000 } }] + for (int i = 0; i < params.length; i++) { + Param pair = params[i]; + if (pair.value instanceof Map) { + Map innerMap = (Map) pair.value; if (innerMap.size() == 1 && innerMap.containsKey("date") && innerMap.get("date") instanceof Double) { Long dateValue = Double.valueOf((Double) innerMap.get("date")).longValue(); - params.put(pair.getKey(), new Date(dateValue)); + params[i] = new Param(pair.name, new Date(dateValue)); } } } } - static void rewriteDecimals(Map params) { + static void rewriteDecimals(Param[] params) { // For each value in `params` that's a map with the single key // `decimal` and a string value s // return a map with that value changed to Decimal(s) // In JSON this looks like: - // "params": {"val": {"decimal": "1234567890123456789.987654321"}}, - for (Map.Entry pair : params.entrySet()) { - if (pair.getValue() instanceof Map) { - Map innerMap = (Map) pair.getValue(); + // "params": [{"name": "val"}, {"value": {"decimal": "1234567890123456789.987654321"}}] + for (int i = 0; i < params.length; i++) { + Param pair = params[i]; + if (pair.value instanceof Map) { + Map innerMap = (Map) pair.value; if (innerMap.size() == 1 && innerMap.containsKey("decimal") && innerMap.get("decimal") instanceof String) { String decimalValue = (String) innerMap.get("decimal"); - params.put(pair.getKey(), new com.ibm.icu.math.BigDecimal(decimalValue)); + params[i] = new Param(pair.name, new com.ibm.icu.math.BigDecimal(decimalValue)); } } } } + static Map paramsToMap(Param[] params) { + if (params == null) { + return null; + } + TreeMap result = new TreeMap(); + for (Param pair : params) { + result.put(pair.name, pair.value); + } + return result; + } - static boolean expectsErrors(Unit unit) { - return unit.errors != null && !unit.errors.isEmpty(); + static boolean expectsErrors(DefaultTestProperties defaults, Unit unit) { + return (unit.expErrors != null && !unit.expErrors.isEmpty()) + || (defaults.expErrors != null && defaults.expErrors.length > 0); } - static void runTestCase(Unit unit) { - runTestCase(unit, null); + static void runTestCase(DefaultTestProperties defaults, Unit unit) { + runTestCase(defaults, unit, null); } - static void runTestCase(Unit unit, Map params) { + static void runTestCase(DefaultTestProperties defaults, Unit unit, Param[] params) { if (unit.ignoreJava != null) { return; } StringBuilder pattern = new StringBuilder(); - if (unit.srcs != null) { - for (String src : unit.srcs) { + if (unit.src != null) { + for (String src : unit.src.sources) { pattern.append(src); } - } else if (unit.src != null) { - pattern.append(unit.src); } // We can call the "complete" constructor with null values, but we want to test that @@ -136,6 +152,8 @@ static void runTestCase(Unit unit, Map params) { MessageFormatter.builder().setPattern(pattern.toString()); if (unit.locale != null && !unit.locale.isEmpty()) { mfBuilder.setLocale(Locale.forLanguageTag(unit.locale)); + } else if (defaults.locale != null) { + mfBuilder.setLocale(Locale.forLanguageTag(defaults.locale)); } else { mfBuilder.setLocale(Locale.US); } @@ -147,16 +165,18 @@ static void runTestCase(Unit unit, Map params) { rewriteDates(params); rewriteDecimals(params); } - String result = mf.formatToString(params); - if (expectsErrors(unit)) { + String result = mf.formatToString(paramsToMap(params)); + if (expectsErrors(defaults, unit)) { fail(reportCase(unit) + "\nExpected error, but it didn't happen.\n" + "Result: '" + result + "'"); } else { - assertEquals(reportCase(unit), unit.exp, result); + if (unit.exp != null) { + assertEquals(reportCase(unit), unit.exp, result); + } } } catch (IllegalArgumentException | NullPointerException e) { - if (!expectsErrors(unit)) { + if (!expectsErrors(defaults, unit)) { fail(reportCase(unit) + "\nNo error was expected here, but it happened:\n" + e.getMessage()); diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java index 3ff18ecd7cc2..810594f29253 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/message2/Unit.java @@ -10,29 +10,27 @@ // Class corresponding to the json test files. // Since this is serialized by Gson, the field names should match the keys in the .json files. class Unit { - final String src; - final List srcs; + // For why this is not an ArrayList, see StringToListAdapter.java + final Sources src; final String locale; - final Map params; + final Param[] params; final String exp; final String ignoreJava; - final List errors; + final List expErrors; Unit( - String src, - List srcs, + Sources src, String locale, - Map params, + Param[] params, String exp, String ignoreJava, - List errors) { + List expErrors) { this.src = src; - this.srcs = srcs; this.locale = locale; this.params = params; this.exp = exp; this.ignoreJava = ignoreJava; - this.errors = errors; + this.expErrors = expErrors; } class Error { @@ -56,7 +54,9 @@ public String toString() { @Override public String toString() { StringJoiner result = new StringJoiner(", ", "UnitTest {", "}"); - result.add("src=" + escapeString(src)); + if (src != null) { + result.add("src=" + src.sources.toString()); + } if (params != null) { result.add("params=" + params); } @@ -75,14 +75,13 @@ public String toString() { * @return a new unit created by merging `this` unit and `other` */ public Unit merge(Unit other) { - String newSrc = other.src != null ? other.src : this.src; - List newSrcs = other.srcs != null ? other.srcs : this.srcs; + Sources newSrc = other.src != null ? other.src : this.src; String newLocale = other.locale != null ? other.locale : this.locale; - Map newParams = other.params != null ? other.params : this.params; + Param[] newParams = other.params != null ? other.params : this.params; String newExp = other.exp != null ? other.exp : this.exp; String newIgnore = other.ignoreJava != null ? other.ignoreJava : this.ignoreJava; - List newErrors = other.errors != null ? other.errors : this.errors; - return new Unit(newSrc, newSrcs, newLocale, newParams, newExp, newIgnore, newErrors); + List newExpErrors = other.expErrors != null ? other.expErrors : this.expErrors; + return new Unit(newSrc, newLocale, newParams, newExp, newIgnore, newExpErrors); } private static String escapeString(String str) { diff --git a/testdata/message2/README.txt b/testdata/message2/README.txt index 17c7c656c320..b803ebc0425e 100644 --- a/testdata/message2/README.txt +++ b/testdata/message2/README.txt @@ -1,19 +1,14 @@ © 2024 and later: Unicode, Inc. and others. License & terms of use: http://www.unicode.org/copyright.html -The format of the JSON files in this directory follows the same format as `test-core.json` -in the spec, described in: +The format of the JSON files in this directory and subdirectories +follow the test schema defined in the Conformance repository: -https://github.com/unicode-org/message-format-wg/blob/main/test/README.md +https://github.com/unicode-org/conformance/blob/main/schema/message_fmt2/testgen_schema.json -The `parts` field is not used. +(as of https://github.com/unicode-org/conformance/pull/255 or later). -# JSON extensions - -An additional `comment` field may be present, which is only for human readers. - -A "srcs" field, whose value is an array of strings, may be present instead -of "src". The strings are concatenated to get the message. +# JSON notes In the "params" field, a date parameter can be expressed as: { "date": n } @@ -46,43 +41,3 @@ If present, "char" reflects the expected character offset and "line" reflects the expected line number in the parse error. The files with "diagnostics" in the name have these fields filled in. -# ICU4C vs. ICU4J tests - -The following tests are run in both ICU4C and ICU4J: - -* alias-selector-annotations.json -* duplicate-declarations.json -* icu-parser-tests.json - - Two tests removed while single-sourcing tests, because a `{{}}` message body - had to be added to get it to parse in ICU4C, and this broke the test in ICU4J. - These tests are in icu-parser-tests-old.json -* icu-test-functions.json - - Some tests marked as ignored -* icu-test-previous-release.json - - Some tests marked as ignored -* icu-test-selectors.json -* markup.json -* matches-whitespace.json - - Some tests marked as ignored -* more-data-model-errors.json -* more-syntax-errors.json -* reserved-syntax.json - - All tests marked as ignored in Java (resolution errors are suppressed) -* resolution-errors.json - - All tests marked as ignored in Java (resolution errors are suppressed) -* runtime-errors.json - - All tests marked as ignored in Java (message function errors are suppressed) -* syntax-errors-diagnostics.json -* tricky-declarations.json -* valid-tests.json - - Some tests marked as ignored -* spec/* - - Some tests in test-core.json and test-functions.json marked as ignored - -The following tests are only run in ICU4C, either because ICU4J doesn't check -for invalid options, or because ICU4J doesn't report line/column numbers for -parse errors: -* invalid-number-literals-diagnostics.json -* invalid-options.json -* syntax-errors-diagnostics-multiline.json -* syntax-errors-end-of-input.json diff --git a/testdata/message2/alias-selector-annotations.json b/testdata/message2/alias-selector-annotations.json index 71ad134d821d..c064d40d4c82 100644 --- a/testdata/message2/alias-selector-annotations.json +++ b/testdata/message2/alias-selector-annotations.json @@ -1,4 +1,10 @@ -[ +{ + "scenario": "Selector annotations", + "description": "Tests for indirectly annotated selectors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".local $one = {|The one| :string}\n .match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", "exp": "Value is not one" @@ -7,4 +13,6 @@ "src": ".local $one = {|The one| :string}\n .local $two = {$one}\n .match {$two}\n 1 {{Value is one}}\n * {{Value is not one}}", "exp": "Value is not one" } -] + ] +} + diff --git a/testdata/message2/duplicate-declarations.json b/testdata/message2/duplicate-declarations.json index 6ea54daa003e..cd3acc1576d3 100644 --- a/testdata/message2/duplicate-declarations.json +++ b/testdata/message2/duplicate-declarations.json @@ -1,25 +1,43 @@ -[ +{ + "scenario": "Duplicate declaration errors", + "description": "Tests that should trigger a duplicate declaration error", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + "tests": [ { "src": ".local $foo = {$foo} .local $foo = {42} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar 42", - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar 42" }, { "src": ".local $foo = {42} .local $foo = {42} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar 42", - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar 42" }, { "src": ".local $foo = {:unknown} .local $foo = {42} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar 42", - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar 42" }, { "src": ".local $x = {42} .local $y = {$x} .local $x = {13} {{{$x} {$y}}}", - "exp": "13 42", - "errors": [{ "type": "duplicate-declaration" }] + "exp": "13 42" + }, + { + "src": ".local $foo = {$foo} {{bar {$foo}}}", + "params": [{ "name": "foo", "value": "foo" }], + "exp": "bar foo" + }, + { + "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", + "params": [{ "name": "baz", "value": "foo" }], + "exp": "bar {$bar}" } -] + ] +} diff --git a/testdata/message2/icu-parser-tests.json b/testdata/message2/icu-parser-tests.json index 0cdfe0a467aa..a28cfc61687f 100644 --- a/testdata/message2/icu-parser-tests.json +++ b/testdata/message2/icu-parser-tests.json @@ -1,61 +1,61 @@ { - "Simple messages": [ - "", - "Hello", - "Hello world!", - "Hello \t \n \r \\{ world!", - "Hello world {:datetime}", - "Hello world {foo}", - "Hello {0} world", - "Hello {123} world", - "Hello {-123} world", - "Hello {3.1416} world", - "Hello {-3.1416} world", - "Hello {123E+2} world", - "Hello {123E-2} world", - "Hello {123.456E+2} world", - "Hello {123.456E-2} world", - "Hello {-123.456E+2} world", - "Hello {-123.456E-2} world", - "Hello {-123E+2} world", - "Hello {-123E-2} world", - "Hello world {$exp}", - "Hello world {$exp :datetime}", - "Hello world {|2024-02-27| :datetime}", - "Hello world {$exp :datetime style=long} and more", - "Hello world {$exp :function number=1234} and more", - "Hello world {$exp :function unquoted=left } and more", - "Hello world {$exp :function quoted=|Something| } and more", - "Hello world {$exp :function quoted=|Something with spaces| } and more", - "Hello world {$exp :function quoted=|Something with \\| spaces and \\| escapes| } and more", - "Hello world {$exp :function number=1234 unquoted=left quoted=|Something|}", - "Hello world {$exp :function number=1234 unquoted=left quoted=|Something longer|}", - "Hello world {$exp :function number=1234 unquoted=left quoted=|Something \\| longer|}" - ], - "Attributes": [ - "Hello world {$exp}", - "Hello world {$exp @attr}", - "Hello world {$exp @valid @attr=a @attrb=123 @atrn=|foo bar|}", - "Hello world {$exp :date @valid @attr=aaaa @attrb=123 @atrn=|foo bar|}", - "Hello world {$exp :date year=numeric month=long day=numeric int=12 @valid @attr=a @attrb=123 @atrn=|foo bar|}" - ], - "Reserved and private": [ - "Reserved {$exp &foo |something more protected|} and more", - "Reserved {$exp %foo |something quoted \\| inside|} and more", - "{{.starting with dot is OK here}}", - "{{Some string pattern, with {$foo} and {$exp :date style=long}!}}" - ], - "Simple messages, with declarations": [ - ".input {$pi :number} {{}}", - ".input {$exp :date} {{}}", - ".local $foo = {$exp} {{}}", - ".local $foo = {$exp :date} {{}}", - ".local $foo = {$exp :date year=numeric month=long day=numeric} {{}}", - ".local $bar = {$foo :date month=medium} {{}}", - ".something |reserved=| {$foo :date} {{}}" - ], - "Multiple declarations in one message": [ - ".input {$a :date} .local $exp = {$a :date style=full} {{Your card expires on {$exp}!}}", - ".input {$a :date} .local $b = {$a :date year=numeric month=long day=numeric} .local $c = {$b :date month=medium} .someting |reserved = \\| and more| {$x :date} {$y :date} {$z :number} {{}}" + "scenario": "Valid tests", + "description": "Additional valid tests", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { "src": "" }, + { "src": "Hello" }, + { "src": "Hello world!" }, + { "src": "Hello \t \n \r \\{ world!" }, + { "src": "Hello world {:datetime}" }, + { "src": "Hello world {foo}" }, + { "src": "Hello {0} world" }, + { "src": "Hello {123} world" }, + { "src": "Hello {-123} world" }, + { "src": "Hello {3.1416} world" }, + { "src": "Hello {-3.1416} world" }, + { "src": "Hello {123E+2} world" }, + { "src": "Hello {123E-2} world" }, + { "src": "Hello {123.456E+2} world" }, + { "src": "Hello {123.456E-2} world" }, + { "src": "Hello {-123.456E+2} world" }, + { "src": "Hello {-123.456E-2} world" }, + { "src": "Hello {-123E+2} world" }, + { "src": "Hello {-123E-2} world" }, + { "src": "Hello world {$exp}" }, + { "src": "Hello world {$exp :datetime}" }, + { "src": "Hello world {|2024-02-27| :datetime}" }, + { "src": "Hello world {$exp :datetime style=long} and more" }, + { "src": "Hello world {$exp :function number=1234} and more" }, + { "src": "Hello world {$exp :function unquoted=left } and more" }, + { "src": "Hello world {$exp :function quoted=|Something| } and more" }, + { "src": "Hello world {$exp :function quoted=|Something with spaces| } and more" }, + { "src": "Hello world {$exp :function quoted=|Something with \\| spaces and \\| escapes| } and more" }, + { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something|}" }, + { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something longer|}" }, + { "src": "Hello world {$exp :function number=1234 unquoted=left quoted=|Something \\| longer|}" }, + { "src": "Hello world {$exp}" }, + { "src": "Hello world {$exp @attr}" }, + { "src": "Hello world {$exp @valid @attr=a @attrb=123 @atrn=|foo bar|}" }, + { "src": "Hello world {$exp :date @valid @attr=aaaa @attrb=123 @atrn=|foo bar|}" }, + { "src": "Hello world {$exp :date year=numeric month=long day=numeric int=12 @valid @attr=a @attrb=123 @atrn=|foo bar|}" }, + { "src": "Reserved {$exp &foo |something more protected|} and more" }, + { "src": "Reserved {$exp %foo |something quoted \\| inside|} and more" }, + { "src": "{{.starting with dot is OK here}}" }, + { "src": "{{Some string pattern \\}, with {$foo} and {$exp :date style=long}!}}" }, + { "src": ".input {$pi :number} {{}}" }, + { "src": ".input {$exp :date} {{}}" }, + { "src": ".local $foo = {$exp} {{}}" }, + { "src": ".local $foo = {$exp :date} {{}}" }, + { "src": ".local $foo = {$exp :date year=numeric month=long day=numeric} {{}}" }, + { "src": ".local $bar = {$foo :date month=medium} {{}}" }, + { "src": ".something |reserved=| {$foo :date} {{}}" }, + { "src": ".input {$a :date} .local $exp = {$a :date style=full} {{Your card expires on {$exp}!}}" }, + { "src": ".input {$a :date} .local $b = {$a :date year=numeric month=long day=numeric} .local $c = {$b :date month=medium} .someting |reserved = \\| and more| {$x :date} {$y :date} {$z :number} {{}}" }, + { "src": ".input {$x :number} {{_}}" }, + { "src": ".local $foo = {|1|} {{_}}" }, + { "src": ".unsupported |statement| {$x :number} {{_}}" } ] } diff --git a/testdata/message2/icu-test-functions.json b/testdata/message2/icu-test-functions.json index 2dfd91cb8030..a97446addf0e 100644 --- a/testdata/message2/icu-test-functions.json +++ b/testdata/message2/icu-test-functions.json @@ -1,95 +1,98 @@ { - "Date and time formats": [ + "scenario": "Function tests", + "description": "Tests for ICU-specific formatting behavior.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "Expires on {$exp}", "exp": "Expires on 8/3/24, 9:43 PM", "comment": "Modified from ICU4J copy to add params (likewise with the other date/time tests); 1722746637000 is 2024-08-03 21:43:57 PDT", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime}", "exp": "Expires on 8/3/24, 9:43 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime icu:skeleton=yMMMMdjmsSSEE}", "exp": "Expires on Sat, August 3, 2024 at 9:43:57.00 PM", - "params": {"exp": { "date": 1722746637000 } }, + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }], "ignoreCpp": "ICU-22754 Skeleton option not implemented yet" }, { "src": "Expires on {$exp :datetime dateStyle=full}", "exp": "Expires on Saturday, August 3, 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime dateStyle=long}", "exp": "Expires on August 3, 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime dateStyle=medium}", "exp": "Expires on Aug 3, 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime timeStyle=long}", "exp": "Expires on 9:43:57 PM PDT", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime timeStyle=medium}", "exp": "Expires on 9:43:57 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime timeStyle=short}", "exp": "Expires on 9:43 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime dateStyle=full timeStyle=medium}", "exp": "Expires on Saturday, August 3, 2024 at 9:43:57 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime year=numeric month=long}", "exp": "Expires on August 2024", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "src": "Expires on {$exp :datetime year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}", "exp": "Expires on 3 Saturday 2024, 9:43 PM", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires on {$exp :date year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}", "exp": "Expires on 3 Saturday 2024", - "params": {"exp": { "date": 1722746637000 } }, + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }], "ignoreCpp": "ICU-22754 ICU4C doesn't accept field options for `:date` or `:time` -- see spec" }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires at {$exp :time year=numeric month=medium day=numeric weekday=long hour=numeric minute=numeric}", "exp": "Expires at 9:43 PM", - "params": {"exp": { "date": 1722746637000 } }, + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }], "ignoreCpp": "ICU-22754 ICU4C doesn't accept field options for `:date` or `:time` -- see spec" }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires at {$exp :time style=long dateStyle=full timeStyle=medium}", "exp": "Expires at 9:43:57 PM PDT", - "params": {"exp": { "date": 1722746637000 } } + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] }, { "comment": "Make sure we ignore date / time fields if needed", "src": "Expires on {$exp :date style=long dateStyle=full timeStyle=medium}", "exp": "Expires on August 3, 2024", - "params": {"exp": { "date": 1722746637000 } } - } - ], - "Literals" : [ + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] + }, { "src": "Expires on {|2025-02-27| :datetime dateStyle=full}", "exp": "Expires on Thursday, February 27, 2025" @@ -115,12 +118,10 @@ "src": "Expires at {|2024-07-02T19:23:45+03:30| :datetime timeStyle=full}", "exp": "Expires at 7:23:45 PM GMT+03:30", "ignoreCpp": "ICU-22754 Time zones not working yet (bug)" - } - ], - "Chaining" : [ + }, { "comment": "Horibly long, but I really wanted to test multiple declarations with overrides, and you can't join strings in JSON", - "srcs": [ + "src": [ ".input {$exp :datetime timeStyle=short}\n", ".input {$user :string}\n", ".local $longExp = {$exp :datetime dateStyle=long}\n", @@ -128,30 +129,30 @@ "{{Hello John, you want '{$exp}', '{$longExp}', or '{$zooExp}' or even '{$exp :datetime dateStyle=full}'?}}" ], "exp": "Hello John, you want '9:43 PM', 'August 3, 2024 at 9:43 PM', or '8/3/24, 9:43:57 PM Pacific Daylight Time' or even 'Saturday, August 3, 2024 at 9:43 PM'?", - "params": {"exp": { "date": 1722746637000 }, "user": "John", "tsOver" : "full" }, + "params": [{"name": "exp", "value": { "date": 1722746637000 }}, + {"name": "user", "value": "John"}, + {"name": "tsOver", "value": "full" }], "ignoreCpp": "ICU-22754 ICU4C doesn't implement this kind of function composition yet. See https://github.com/unicode-org/message-format-wg/issues/515" }, { - "srcs": [ + "src": [ ".input {$exp :datetime year=numeric month=numeric day=|2-digit|}\n", ".local $longExp = {$exp :datetime month=long weekday=long}\n", "{{Expires on '{$exp}' ('{$longExp}').}}" ], "exp": "Expires on '8/03/2024' ('Saturday, August 03, 2024').", - "params": {"exp": { "date": 1722746637000 } } - } - ], - "Number formatter" : [ + "params": [{ "name": "exp", "value": { "date": 1722746637000 } }] + }, { "src": "Format {$val} number", - "params": { "val": 31 }, + "params": [{ "name": "val", "value": 31 }], "exp": "Format 31 number" }, { "src": "Format {123456789.9876} number", "locale": "en-IN", - "exp": "Format 12,34,56,789.9876 number", - "ignoreCpp": "ICU-22754 No default formatting for numbers, so it's formatted as a literal string. Is this in the spec?" + "exp": "Format 123456789.9876 number", + "comment": "Number literals are not formatted as numbers by default" }, { "src": "Format {|3.1416|} number", @@ -161,8 +162,8 @@ { "src": "Format {|3.1416|} number", "locale": "ar-AR-u-nu-arab", - "exp": "Format ٣٫١٤١٦ number", - "ignoreCpp": "ICU-22754 No default formatting for numbers, so it's formatted as a literal string. Is this in the spec?" + "exp": "Format 3.1416 number", + "comment": "Number literals are not formatted as numbers by default" }, { "src": "Format {3.1415926 :number}", diff --git a/testdata/message2/icu-test-previous-release.json b/testdata/message2/icu-test-previous-release.json index cc863411db95..0a1e27dff6e2 100644 --- a/testdata/message2/icu-test-previous-release.json +++ b/testdata/message2/icu-test-previous-release.json @@ -1,38 +1,10 @@ -[ - { - "src": "hello", - "exp": "hello" - }, - { - "src": "hello {|world|}", - "exp": "hello world" - }, - { - "src": "hello {||}", - "exp": "hello " - }, - { - "src": "hello {$place}", - "params": { "place": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place}", - "exp": "hello {$place}", - "errors": [{ "type": "unresolved-var" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{$one} and {$two}", - "params" : { "one": 1.3, "two": 4.2 }, - "exp": "1.3 and 4.2" - }, - { - "src": "{$one} et {$two}", - "locale": "fr", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1,3 et 4,2" - }, +{ + "scenario": "Tests from original ICU4J release", + "description": "Tests taken from the September 2022 MF2 ICU4J release", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "hello {|4.2| :number}", "exp": "hello 4.2" @@ -43,254 +15,108 @@ "exp": "hello \u0664\u066B\u0662" }, { - "src": "hello {|foo| :number}", - "exp": "hello {|foo|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {:number}", - "exp": "hello {:number}", - "comment": "This is different from JS, should be an error.", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=2}", - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=|2|}", - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", - "params": { "foo": 2.0 }, - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", - "params": { "foo": "2" }, - "exp": "hello 4.20", - "errorsJs": ["invalid-type"] - }, - { - "src": ".local $foo = {|bar|} {{bar {$foo}}}", - "exp": "bar bar" - }, - { + "comment": "This is not an error! foo is not used before the local declaration, so the local declaration of $foo shadows the input variable.", "src": ".local $foo = {bar} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "expectedJs": "bar foo", - "comment": "It is undefined if we allow arguments to override local variables, or it is an error. And undefined who wins if that happens, the local variable of the argument.", - "exp": "bar bar" - }, - { - "src": ".local $foo = {$bar} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar foo" + "exp": "bar bar", + "params": [{ "name": "foo", "value": "foo" }] }, { "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": 4.2 }, + "params": [{ "name": "bar", "value": 4.2 }], "exp": "bar 4.2" }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.20" - }, - { - "ignoreJava": "Maybe. Because `minimumFractionDigits=foo`", - "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.2", - "errors": [{ "type": "bad-option" }] - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, { "src": ".local $bar = {$baz} .local $foo = {$bar} {{bar {$foo}}}", - "params": { "baz": "foo" }, + "params": [{ "name": "baz", "value": "foo" }], "exp": "bar foo" }, - { - "patternJs": ".match {$foo} 1 {{one}} * {{other}}", - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, { "src": ".match {$foo :number} 1 {{one}} * {{other}}", - "params": { "foo": "1" }, + "params": [{ "name": "foo", "value": "1" }], "exp": "one", "ignoreJava": "See ICU-22809" }, { "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, - { - "patternJs": ".match {$foo} 1 {{one}} * {{other}}", - "src": ".match {$foo :number} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, + "params": [{ "name": "foo", "value": "1" }], "exp": "one" }, { "src": ".match {$foo :number} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, + "params": [{ "name": "foo", "value": 1 }], "exp": "one" }, { "ignoreJava": "Can't pass null in a map", + "ignoreCpp": "Same as Java", "src": ".match {$foo} 1 {{one}} * {{other}}", - "params": { "foo": null }, + "params": [{ "name": "foo", "value": null }], "exp": "other" }, { - "srcJs": ".match {$foo} 1 {{one}} * {{other}}", "src": ".match {$foo :number} 1 {{one}} * {{other}}", "exp": "other", - "errors": [{ "type": "missing-var" }] - }, - { - "srcJs": ".match {$foo} one {{one}} * {{other}}", - "src": ".match {$foo :number} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "srcJs": ".match {$foo} 1 {{=1}} one {{one}} * {{other}}", - "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "srcJs": ".match {$foo} one {{one}} 1 {{=1}} * {{other}}", - "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 1 }, - "exp": "one one" - }, - { - "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 2 }, - "exp": "one other" - }, - { - "srcJs": ".match {$foo} {$bar} one one {{one one}} one * {{one other}} * * {{other}}", - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 2, "bar": 2 }, - "exp": "other" + "expErrors": [{ "type": "unresolved-variable" }] }, { - "srcJs": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, + "params": [{ "name": "bar", "value": 1 }], "exp": "one" }, { - "srcJs": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, + "params": [{ "name": "bar", "value": 2 }], "exp": "other" }, { - "srcJs": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 1, "none": "" }, + "params": [{ "name": "foo", "value": 1 }, {"name": "none", "value": "" }], "exp": "one" }, { - "srcJs": ".local $bar = {$none} .match {$foo} one {{one}} * {{{$bar}}}", "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}", - "params": { "foo": 2 }, + "params": [{ "name": "foo", "value": 2 }], "exp": "{$none}", - "errors": [{ "type": "unresolved-var" }], + "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, { "src": "{{#tag}}", "exp": "#tag" }, - { - "src": "{#tag}", - "exp": "" - }, - { - "src": "{#tag}content", - "exp": "content" - }, { "src": "{#tag}content{/tag}", "exp": "content" }, - { - "src": "{#tag}content", - "exp": "content" - }, - { - "comment": "When we format markup to string we generate no output", - "src": "{#tag foo=bar}", - "exp": "" - }, { "src": "{#tag foo=foo bar=$bar}", - "params": { "bar": "b a r" }, + "params": [{ "name": "bar", "value": "b a r" }], "exp": "" }, { "src": "bad {#markup/} test", - "exp": "bad test", - "errorsJs": [{ "type": "extra-content" }] - }, - { - "src": "{#tag foo=bar}", - "exp": "", - "errorsJs": [{ "type": "extra-content" }] - }, - { - "src": "no braces", - "exp": "no braces", - "errorsJs": [{ "type": "parse-error" }, { "type": "junk-element" }] + "exp": "bad test" }, { "src": "no braces {$foo}", - "params": { "foo": 2 }, - "exp": "no braces 2", - "errorsJs": [{ "type": "parse-error" }, { "type": "junk-element" }] + "params": [{ "name": "foo", "value": 2 }], + "exp": "no braces 2" }, { "src": "empty { }", "exp": "empty ", - "errors": [{ "type": "parse-error" }, { "type": "junk-element" }], + "expErrors": [{ "type": "syntax-error" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/703" }, { "src": "bad {:}", "exp": "bad {:}", - "errors": [{ "type": "empty-token" }, { "type": "missing-func" }] - }, - { - "src": "bad {placeholder}", - "exp": "bad placeholder", - "errorsJs": [{ "type": "parse-error" }, { "type": "extra-content" }, { "type": "junk-element" }] + "expErrors": [{ "type": "syntax-error" }, { "type": "unknown-function" }] }, { "src": "{bad {$placeholder option}}", "exp": "bad {$placeholder}", - "errors": [{ "type": "extra-content" }, { "type": "extra-content" }, { "type": "missing-var" }], + "expErrors": [{ "type": "syntax-error"}, { "type": "unresolved-variable" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/703" }, { @@ -300,14 +126,14 @@ { "src": ".match {$foo :string} * * {{foo}}", "exp": "foo", - "errors": [{ "type": "key-mismatch" }, { "type": "missing-var" }], + "expErrors": [{ "type": "variant-key-mismatch" }, { "type": "unresolved-variable" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/735" }, { "src": ".match {$foo :string} {$bar :string} * {{foo}}", "exp": "foo", - "errors": [{ "type": "key-mismatch" }, { "type": "missing-var" }, { "type": "missing-var" }], + "expErrors": [{ "type": "variant-key-mismatch" }, { "type": "unresolved-variable" }], "ignoreCpp": "Fallback is unclear. See https://github.com/unicode-org/message-format-wg/issues/735" } -] - + ] +} diff --git a/testdata/message2/icu-test-selectors.json b/testdata/message2/icu-test-selectors.json index efe67e7c4c9e..102bdfd88f50 100644 --- a/testdata/message2/icu-test-selectors.json +++ b/testdata/message2/icu-test-selectors.json @@ -1,46 +1,171 @@ -[ +{ + "scenario": "Match tests", + "description": "Tests for various match constructs", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "comment": "Testing simple plural", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 0}], + "exp": "0 files" + }, { "comment": "Testing simple plural", - "shared": { - "srcs": [ + "src": [ ".match {$count :number}\n", "one {{{$count} file}}\n", " * {{{$count} files}}" - ] - }, - "variations" : [ - { "params": { "count": 0 }, "exp": "0 files" }, - { "params": { "count": 1 }, "exp": "1 file" }, - { "params": { "count": 3 }, "exp": "3 files" }, - { "params": { "count": 0 }, "locale": "fr", "exp": "0 file" }, - { "params": { "count": 1 }, "locale": "fr", "exp": "1 file" }, - { "params": { "count": 3 }, "locale": "fr", "exp": "3 files" } - ] + ], + "params": [{"name": "count", "value": 1}], + "exp": "1 file" + }, + { + "comment": "Testing simple plural", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 3}], + "exp": "3 files" + }, + { + "comment": "Testing simple plural", + "locale": "fr", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 0}], + "exp": "0 file" + }, + { + "comment": "Testing simple plural", + "locale": "fr", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 1}], + "exp": "1 file" + }, + { + "comment": "Testing simple plural", + "locale": "fr", + "src": [ + ".match {$count :number}\n", + "one {{{$count} file}}\n", + " * {{{$count} files}}" + ], + "params": [{"name": "count", "value": 3}], + "exp": "3 files" + }, + { + "comment": "Testing simple plural, but swap variant order", + "src": [ + ".match {$count :number}\n", + " * {{You deleted {$count} files}}\n", + "one {{You deleted {$count} file}}" + ], + "params": [{"name": "count", "value": 1}], + "exp": "You deleted 1 file" }, { "comment": "Testing simple plural, but swap variant order", - "shared": { - "srcs": [ + "src": [ ".match {$count :number}\n", " * {{You deleted {$count} files}}\n", "one {{You deleted {$count} file}}" - ] - }, - "variations" : [ - { - "params": { "count": 1 }, - "exp": "You deleted 1 file" - }, - { - "params": { "count": 3 }, - "exp": "You deleted 3 files" - } - ] + ], + "params": [{"name": "count", "value": 3}], + "exp": "You deleted 3 files" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 1}], + "exp": "You got the gold medal" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 2}], + "exp": "You got the silver medal" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 3}], + "exp": "You got the bronze medal" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 7}], + "exp": "You finished in the 7th place" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 21}], + "exp": "You finished in the 21st place" }, { "comment": "Ordinal, with mixed order and exact matches", - "shared": { - "srcs": [ + "src": [ ".match {$place :number select=ordinal}\n", "* {{You finished in the {$place}th place}}\n", "two {{You finished in the {$place}nd place}}\n", @@ -49,83 +174,199 @@ "2 {{You got the silver medal}}\n", "3 {{You got the bronze medal}}\n", "few {{You finished in the {$place}rd place}}" - ] - }, - "variations" : [ - { "params": { "place": 1 }, "exp": "You got the gold medal" }, - { "params": { "place": 2 }, "exp": "You got the silver medal" }, - { "params": { "place": 3 }, "exp": "You got the bronze medal" }, - { "params": { "place": 7 }, "exp": "You finished in the 7th place" }, - { "params": { "place": 21 }, "exp": "You finished in the 21st place" }, - { "params": { "place": 22 }, "exp": "You finished in the 22nd place" }, - { "params": { "place": 23 }, "exp": "You finished in the 23rd place" }, - { "params": { "place": 28 }, "exp": "You finished in the 28th place" } - ] + ], + "params": [{"name": "place", "value": 22}], + "exp": "You finished in the 22nd place" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 23}], + "exp": "You finished in the 23rd place" + }, + { + "comment": "Ordinal, with mixed order and exact matches", + "src": [ + ".match {$place :number select=ordinal}\n", + "* {{You finished in the {$place}th place}}\n", + "two {{You finished in the {$place}nd place}}\n", + "one {{You finished in the {$place}st place}}\n", + "1 {{You got the gold medal}}\n", + "2 {{You got the silver medal}}\n", + "3 {{You got the bronze medal}}\n", + "few {{You finished in the {$place}rd place}}" + ], + "params": [{"name": "place", "value": 28}], + "exp": "You finished in the 28th place" + }, + { + "comment": "Plural combinations, mixed order", + "src": [ + ".match {$fileCount :number} {$folderCount :number}\n", + " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", + " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", + " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", + " * one {{You found {$fileCount} files in {$folderCount} folder}}" + ], + "params": [{"name": "fileCount", "value": 1}, + {"name": "folderCount", "value": 1}], + "exp": "You found 1 file in 1 folder" + }, + { + "comment": "Plural combinations, mixed order", + "src": [ + ".match {$fileCount :number} {$folderCount :number}\n", + " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", + " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", + " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", + " * one {{You found {$fileCount} files in {$folderCount} folder}}" + ], + "params": [{"name": "fileCount", "value": 1}, + {"name": "folderCount", "value": 5}], + "exp": "You found 1 file in 5 folders" + }, + { + "comment": "Plural combinations, mixed order", + "src": [ + ".match {$fileCount :number} {$folderCount :number}\n", + " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", + " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", + " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", + " * one {{You found {$fileCount} files in {$folderCount} folder}}" + ], + "params": [{"name": "fileCount", "value": 7}, + {"name": "folderCount", "value": 1}], + "exp": "You found 7 files in 1 folder" }, { "comment": "Plural combinations, mixed order", - "shared": { - "srcs": [ + "src": [ ".match {$fileCount :number} {$folderCount :number}\n", " * * {{You found {$fileCount} files in {$folderCount} folders}}\n", " one one {{You found {$fileCount} file in {$folderCount} folder}}\n", " one * {{You found {$fileCount} file in {$folderCount} folders}}\n", " * one {{You found {$fileCount} files in {$folderCount} folder}}" - ] - }, - "variations" : [ - { "params": { "fileCount": 1, "folderCount": 1 }, "exp": "You found 1 file in 1 folder" }, - { "params": { "fileCount": 1, "folderCount": 5 }, "exp": "You found 1 file in 5 folders" }, - { "params": { "fileCount": 7, "folderCount": 1 }, "exp": "You found 7 files in 1 folder" }, - { "params": { "fileCount": 7, "folderCount": 3 }, "exp": "You found 7 files in 3 folders" } - ] + ], + "params": [{"name": "fileCount", "value": 7}, + {"name": "folderCount", "value": 3}], + "exp": "You found 7 files in 3 folders" + }, + { + "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", + "src": [ + ".local $c = {$price :number minimumFractionDigits=$minF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1 }, + { "name": "minF", "value": 0 }], + "exp": "1 dollar" }, { "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", - "shared": { - "srcs": [ + "src": [ ".local $c = {$price :number minimumFractionDigits=$minF}\n", ".match {$c}\n", " one {{{$c} dollar}}\n", " * {{{$c} dollars}}" - ] - }, - "variations" : [ - { "params": { "price": 1, "minF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1, "minF": 2 }, "exp": "1.00 dollars" } - ] + ], + "params": [{ "name": "price", "value": 1}, + { "name": "minF", "value": 2 }], + "exp": "1.00 dollars" }, { "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", - "shared": { - "srcs": [ + "src": [ ".local $c = {$price :number maximumFractionDigits=$maxF}\n", ".match {$c}\n", " one {{{$c} dollar}}\n", " * {{{$c} dollars}}" - ] - }, - "variations" : [ - { "params": { "price": 1.25, "maxF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1.25, "maxF": 2 }, "exp": "1.25 dollars" } - ] + ], + "params": [{ "name": "price", "value": 1.25}, + { "name": "maxF", "value": 0 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the formatting option (`1.00 dollars`)", + "src": [ + ".local $c = {$price :number maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1.25}, + { "name": "maxF", "value": 2 }], + "exp": "1.25 dollars" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1}, + { "name": "maxF", "value": 0 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1}, + { "name": "maxF", "value": 2 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1.25}, + { "name": "maxF", "value": 0 }], + "exp": "1 dollar" + }, + { + "comment": "Test that the selection honors the `:integer` over options", + "src": [ + ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", + ".match {$c}\n", + " one {{{$c} dollar}}\n", + " * {{{$c} dollars}}" + ], + "params": [{ "name": "price", "value": 1.25 }, + { "name": "maxF", "value": 2 }], + "exp": "1 dollar" }, { "comment": "Test that the selection honors the `:integer` over options", - "shared": { - "srcs": [ + "src": [ ".local $c = {$price :integer maximumFractionDigits=$maxF}\n", ".match {$c}\n", " one {{{$c} dollar}}\n", " * {{{$c} dollars}}" - ] - }, - "variations" : [ - { "params": { "price": 1, "maxF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1, "maxF": 2 }, "exp": "1 dollar" }, - { "params": { "price": 1.25, "maxF": 0 }, "exp": "1 dollar" }, - { "params": { "price": 1.25, "maxF": 2 }, "exp": "1 dollar" }, - { "params": { "price": 4.12345, "maxF": 4 }, "exp": "4 dollars" } - ] + ], + "params": [{ "name": "price", "value": 4.12345 }, + { "name": "maxF", "value": 4 }], + "exp": "4 dollars" } -] + ] +} diff --git a/testdata/message2/invalid-number-literals-diagnostics.json b/testdata/message2/invalid-number-literals-diagnostics.json index 1c1e84f53f37..d35c16b23386 100644 --- a/testdata/message2/invalid-number-literals-diagnostics.json +++ b/testdata/message2/invalid-number-literals-diagnostics.json @@ -1,4 +1,15 @@ -[ +{ + "scenario": "Number literal syntax errors", + "description": "Syntax errors with number literals; for ICU4C, the character offset in the parse error is checked", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ { "src": "{00}", "char": 2}, { "src": "{042}", "char": 2}, { "src": "{1.}", "char": 3}, @@ -10,4 +21,5 @@ { "src": "{1e+}", "char": 4}, { "src": "{1e-}", "char": 4}, { "src": "{1.0e2.0}", "char": 6} -] + ] +} diff --git a/testdata/message2/invalid-options.json b/testdata/message2/invalid-options.json index 8b4740ee9bcb..698583cd5578 100644 --- a/testdata/message2/invalid-options.json +++ b/testdata/message2/invalid-options.json @@ -1,34 +1,47 @@ -[ - { "src": ".local $foo = {1 :number minimumIntegerDigits=-1} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], +{ + "scenario": "Bad options for built-in functions", + "description": "Tests for the bad-option error; only run in ICU4C for now", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "bad-option" + } + ] + }, + "tests": [ + { "comment": "Neither impl validates options right now; see https://github.com/unicode-org/message-format-wg/issues/738", + "src": ".local $foo = {1 :number minimumIntegerDigits=-1} {{bar {$foo}}}", + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number minimumIntegerDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number minimumFractionDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number maximumFractionDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number minimumSignificantDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :number maximumSignificantDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :integer minimumIntegerDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" }, { "src": ".local $foo = {1 :integer maximumSignificantDigits=foo} {{bar {$foo}}}", - "errors": [{"type": "bad-option"}], + "ignoreCpp": "ICU4C doesn't validate options", "ignoreJava": "ICU4J doesn't validate options" } -] + ] +} diff --git a/testdata/message2/markup.json b/testdata/message2/markup.json index 52c86b7aa637..29b4408e2c4a 100644 --- a/testdata/message2/markup.json +++ b/testdata/message2/markup.json @@ -1,14 +1,15 @@ -[ - { "src": "{#tag}", "exp": "" }, +{ + "scenario": "Markup", + "description": "Tests for valid markup strings", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "{#tag/}", "exp": "" }, { "src": "{/tag}", "exp": "" }, - { "src": "{#tag}content", "exp": "content" }, { "src": "{#tag}content{/tag}", "exp": "content" }, - { "src": "{/tag}content", "exp": "content" }, - { "src": "{#tag foo=bar}", "exp": "" }, - { "src": "{/tag foo=bar}", "exp": "" }, - { "src": "{#tag foo=bar/}", "exp": "" }, { "src": "{#tag foo=|foo| bar=$bar}", - "params": { "bar": "b a r" }, + "params": [{ "name": "bar", "value": "b a r" }], "exp": "" } -] + ] +} diff --git a/testdata/message2/matches-whitespace.json b/testdata/message2/matches-whitespace.json index d0b2c4ecdfe7..a0af4c4d143e 100644 --- a/testdata/message2/matches-whitespace.json +++ b/testdata/message2/matches-whitespace.json @@ -1,4 +1,10 @@ -[ +{ + "scenario": "Matches with whitespace", + "description": "Tests for valid match constructs with whitespace in various places", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".match {one :string} {bar :string} one * {{one}} * * {{other}}", "exp": "one" }, { "src": ".match {foo :string} {bar :string}one * {{one}} * * {{other}}", @@ -23,4 +29,6 @@ }, { "src": ".match {foo :string} {bar :string} one * {{one}} * * {{foo}}", "exp": "foo" } -] + ] +} + diff --git a/testdata/message2/more-data-model-errors.json b/testdata/message2/more-data-model-errors.json index 0c86c58a5a12..14c33c66d353 100644 --- a/testdata/message2/more-data-model-errors.json +++ b/testdata/message2/more-data-model-errors.json @@ -1,34 +1,205 @@ { - "Variant Key Mismatch": [ - ".match {$foo :number} {$bar :number} one{{one}}", - ".match {$foo :number} {$bar :number} one {{one}}", - ".match {$foo :number} {$bar :number} one {{one}}", - ".match {$foo :number} * * {{foo}}", - ".match {$one :number}\n 1 2 {{Too many}}\n * {{Otherwise}}", - ".match {$one :number} {$two :number}\n 1 2 {{Two keys}}\n * {{Missing a key}}\n * * {{Otherwise}}", - ".match {$foo :x} {$bar :x} * {{foo}}" - ], - "Missing Fallback Variant": [ - ".match {$one :number}\n 1 {{Value is one}}\n 2 {{Value is two}}", - ".match {$one :number} {$two :number}\n 1 * {{First is one}}\n * 1 {{Second is one}}" - ], - "Missing Selector Annotation": [ - ".match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", - ".local $one = {|The one|}\n .match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", - ".match {|horse| ^private}\n 1 {{The value is one.}} * {{The value is not one.}}", - ".match {$foo !select} |1| {{one}} * {{other}}", - ".match {$foo ^select} |1| {{one}} * {{other}}", - ".input {$foo} .match {$foo} one {{one}} * {{other}}", - ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}" - ], - "Duplicate Declaration": [ - ".local $x = {|1|} .input {$x :number} {{{$x}}}", - ".input {$x :number} .input {$x :string} {{{$x}}}" - ], - "Duplicate Option Name": [ - "{:foo a=1 b=2 a=1}", - "{:foo a=1 a=1}", - "{:foo a=1 a=2}", - "{|x| :foo a=1 a=2}" - ] + "scenario": "Additional data model errors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": ".match {$foo :number} {$bar :number} one{{one}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :number} {$bar :number} one {{one}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :number} {$bar :number} one {{one}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :number} * * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$one :number}\n 1 2 {{Too many}}\n * {{Otherwise}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$one :number} {$two :number}\n 1 2 {{Two keys}}\n * {{Missing a key}}\n * * {{Otherwise}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$foo :x} {$bar :x} * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + + { + "src": ".match {$one :number}\n 1 {{Value is one}}\n 2 {{Value is two}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + + { + "src": ".match {$one :number} {$two :number}\n 1 * {{First is one}}\n * 1 {{Second is one}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + + { + "src": ".match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".local $one = {|The one|}\n .match {$one}\n 1 {{Value is one}}\n * {{Value is not one}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".match {|horse| ^private}\n 1 {{The value is one.}} * {{The value is not one.}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".match {$foo !select} |1| {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".match {$foo ^select} |1| {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + + { + "src": ".local $x = {|1|} .input {$x :number} {{{$x}}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + + { + "src": ".input {$x :number} .input {$x :string} {{{$x}}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + + { + "src": "{:foo a=1 b=2 a=1}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + + { + "src": "{:foo a=1 a=1}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + + { + "src": "{:foo a=1 a=2}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + + { + "src": "{|x| :foo a=1 a=2}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + } + ] } diff --git a/testdata/message2/more-functions.json b/testdata/message2/more-functions.json index 99640a4daed3..83a55151f786 100644 --- a/testdata/message2/more-functions.json +++ b/testdata/message2/more-functions.json @@ -1,5 +1,10 @@ { - "number": [ + "scenario": "Function tests 2", + "description": "More tests for ICU-specific formatting behavior.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "Format {123456789.9876 :number} number", "locale": "en-IN", @@ -19,54 +24,54 @@ "comment": "From ICU4J tests, with explicit formatter added" }, { - "srcs": [".local $dateStr = {$date :datetime}\n", + "src": [".local $dateStr = {$date :datetime}\n", "{{Testing date formatting: {$dateStr :datetime}.}}"], "exp": "Testing date formatting: 23.11.2022, 19:42.", "locale": "ro", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 }}] }, { "src": "Testing date formatting: {$date :date style=long}.", "exp": "Testing date formatting: November 23, 2022.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :date style=medium}.", "exp": "Testing date formatting: Nov 23, 2022.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :date style=short}.", "exp": "Testing date formatting: 11/23/22.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :time style=long}.", "exp": "Testing date formatting: 7:42:37\u202FPM PST.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :time style=medium}.", "exp": "Testing date formatting: 7:42:37\u202FPM.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { "src": "Testing date formatting: {$date :time style=short}.", "exp": "Testing date formatting: 7:42\u202FPM.", - "params": {"date": {"date": 1669261357000}} + "params": [{ "name": "date", "value": { "date": 1669261357000 } }] }, { - "srcs": [".local $num = {|42| :number}\n", + "src": [".local $num = {|42| :number}\n", "{{Testing date formatting: {$num :datetime}}}"], "exp": "Testing date formatting: {|42|}", - "errors": [{"type": "bad-input"}] + "expErrors": [{"type": "bad-operand"}] }, { "src": "From literal: {|123456789,531| :number}!", "exp": "From literal: {|123456789,531|}!", "locale": "en-IN", - "params": {"val": 1234567890.97531}, - "errors": [{"type": "bad-input"}], + "params": [{ "name": "val", "value": 1234567890.97531 }], + "expErrors": [{"type": "bad-operand"}], "comment": "Should fail because number literals are not treated as localized numbers", "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, @@ -74,32 +79,32 @@ "src": "From literal: {|123456789.531| :number}!", "exp": "From literal: \u1041\u1042\u1043,\u1044\u1045\u1046,\u1047\u1048\u1049.\u1045\u1043\u1041!", "locale": "my", - "params": {"val": 1234567890.97531} + "params": [{ "name": "val", "value": 1234567890.97531 }] }, { "src": "Default double: {$val}!", "exp": "Default double: 1,23,45,67,890.97531!", "locale": "en-IN", - "params": {"val": 1234567890.97531}, + "params": [{ "name": "val", "value": 1234567890.97531 }], "comment": "The next few tests check that numeric variables are formatted without specifying :number" }, { "src": "Default double: {$val}!", "exp": "Default double: 1.234.567.890,97531!", "locale": "ro", - "params": {"val": 1234567890.97531} + "params": [{ "name": "val", "value": 1234567890.97531 }] }, { "src": "Default float: {$val}!", "exp": "Default float: 3,141593!", "locale": "ro", - "params": {"val": 3.1415926535} + "params": [{"name": "val", "value": 3.1415926535}] }, { "src": "Default int64: {$val}!", "exp": "Default int64: 1.234.567.890.123.456.800!", "locale": "ro", - "params": {"val": 1234567890123456789}, + "params": [{ "name": "val", "value": 1234567890123456789 }], "comment": "Rounded due to JSON not supporting full 64-bit ints", "ignoreJava": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175932" }, @@ -107,7 +112,7 @@ "src": "Default int64: {$val}!", "exp": "Default int64: 1.234.567.890.123.456.770!", "locale": "ro", - "params": {"val": 1234567890123456789}, + "params": [{ "name": "val", "value": 1234567890123456789 }], "comment": "Rounded due to JSON not supporting full 64-bit ints", "ignoreCpp": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175932" }, @@ -115,7 +120,7 @@ "src": "Default number: {$val}!", "exp": "Default number: 1.234.567.890.123.456.789,987654!", "locale": "ro", - "params": {"val": {"decimal": "1234567890123456789.987654321"}} + "params": [{ "name": "val", "value": {"decimal": "1234567890123456789.987654321"} }] } ] } diff --git a/testdata/message2/more-syntax-errors.json b/testdata/message2/more-syntax-errors.json deleted file mode 100644 index 48f145b452c0..000000000000 --- a/testdata/message2/more-syntax-errors.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - ".input {$x :number}", - ".local $foo = {|1|}", - ".unsupported |statement| {$x :number}" -] diff --git a/testdata/message2/reserved-syntax.json b/testdata/message2/reserved-syntax.json deleted file mode 100644 index 9efd6fe6b9c9..000000000000 --- a/testdata/message2/reserved-syntax.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { "src": "hello {|4.2| %number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| %n|um|ber}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{+42}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| &num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| ^num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| +num|be|r}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|4.2| ?num|be||r|s}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|foo| !number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {|foo| *number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {?number}", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {$foo ~xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {$x xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{ !xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{~xyzz }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "{ num x \\\\ abcde |aaa||3.14||42| r }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" }, - { "src": "hello {$foo >num x \\\\ abcde |aaa||3.14| |42| r }", "errors": [{ "type": "unsupported-annotation" }], "ignoreJava": "ICU4J doesn't error out on reserved annotations" } -] - diff --git a/testdata/message2/resolution-errors.json b/testdata/message2/resolution-errors.json index 077e8b0e9c3b..610588e0cf5e 100644 --- a/testdata/message2/resolution-errors.json +++ b/testdata/message2/resolution-errors.json @@ -1,14 +1,21 @@ -[ - { "src": "{$oops}", "exp": "{$oops}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "ICU4J doesn't signal unresolved variable errors?"}, - { "src": ".input {$x :number} {{{$x}}}", "exp": "{$x}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "exp": "other", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}", "exp": "{$none}", "errors": [{ "type": "unresolved-var" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": "The value is {horse :func}.", "exp": "The value is {|horse|}.", "errors": [{ "type": "missing-func" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, - { "src": ".matc {-1} {{hello}}", - "errors": [{ "type": "unsupported-statement" }], +{ + "scenario": "Resolution errors", + "description": "Tests for unknown variables and functions", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { "src": "{$oops}", "exp": "{$oops}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "ICU4J doesn't signal unresolved variable errors?"}, + { "src": ".input {$x :number} {{{$x}}}", "exp": "{$x}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": ".local $foo = {$bar} .match {$foo :number} one {{one}} * {{other}}", "exp": "other", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": ".local $bar = {$none :number} .match {$foo :string} one {{one}} * {{{$bar}}}", "exp": "{$none}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": "The value is {horse :func}.", "exp": "The value is {|horse|}.", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782"}, + { "src": ".matc {-1} {{hello}}", + "expErrors": [{ "type": "unsupported-statement" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": ".m {-1} {{hello}}", - "errors": [{ "type": "unsupported-statement" }], + { "src": ".m {-1} {{hello}}", + "expErrors": [{ "type": "unsupported-statement" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" } -] + ] +} diff --git a/testdata/message2/runtime-errors.json b/testdata/message2/runtime-errors.json index 9c7bfbc58443..b1bb0cd491a0 100644 --- a/testdata/message2/runtime-errors.json +++ b/testdata/message2/runtime-errors.json @@ -1,26 +1,27 @@ -[ +{ + "scenario": "Runtime errors", + "description": "Tests for bad-selector and bad-operand errors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".match {|horse| :date}\n 1 {{The value is one.}}\n * {{Formatter used as selector.}}", "exp": "Formatter used as selector.", - "errors": [{"type": "selector-error"}], - "ignoreJava": "ICU4J doesn't signal runtime errors?" - }, - { - "src": ".match {|horse| :string}\n 1 {{The value is one.}}\n * {{Selector used as formatter: {|horse| :string}}}", - "exp": "Selector used as formatter: {|horse|}", - "errors": [{"type": "formatting-error"}], + "expErrors": [{"type": "bad-selector"}], "ignoreJava": "ICU4J doesn't signal runtime errors?" }, { "src": ".match {|horse| :number}\n 1 {{The value is one.}}\n * {{horse is not a number.}}", "exp": "horse is not a number.", - "errors": [{"type": "selector-error"}], + "expErrors": [{"type": "bad-selector"}], "ignoreJava": "ICU4J doesn't signal runtime errors?" }, { "src": ".local $sel = {|horse| :number}\n .match {$sel}\n 1 {{The value is one.}}\n * {{horse is not a number.}}", "exp": "horse is not a number.", - "errors": [{"type": "selector-error"}], + "expErrors": [{"type": "bad-selector"}], "ignoreJava": "ICU4J doesn't signal runtime errors?" } -] + ] +} diff --git a/testdata/message2/spec/data-model-errors.json b/testdata/message2/spec/data-model-errors.json index 0a6bd67641b6..86a674c43961 100644 --- a/testdata/message2/spec/data-model-errors.json +++ b/testdata/message2/spec/data-model-errors.json @@ -1,32 +1,185 @@ { - "Variant Key Mismatch": [ - ".match {$foo :x} * * {{foo}}", - ".match {$foo :x} {$bar :x} * {{foo}}" - ], - "Missing Fallback Variant": [ - ".match {:foo} 1 {{_}}", - ".match {:foo} other {{_}}", - ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}" - ], - "Missing Selector Annotation": [ - ".match {$foo} one {{one}} * {{other}}", - ".input {$foo} .match {$foo} one {{one}} * {{other}}", - ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}" - ], - "Duplicate Declaration": [ - ".input {$foo} .input {$foo} {{_}}", - ".input {$foo} .local $foo = {42} {{_}}", - ".local $foo = {42} .input {$foo} {{_}}", - ".local $foo = {:unknown} .local $foo = {42} {{_}}", - ".local $foo = {$bar} .local $bar = {42} {{_}}", - ".local $foo = {$foo} {{_}}", - ".local $foo = {$bar} .local $bar = {$baz} {{_}}", - ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}", - ".local $foo = {42 :func opt=$foo} {{_}}", - ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}" - ], - "Duplicate Option Name": [ - "bad {:placeholder option=x option=x}", - "bad {:placeholder ns:option=x ns:option=y}" - ] + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "scenario": "Data model errors", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": ".match {$foo :x} * * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + { + "src": ".match {$foo :x} {$bar :x} * {{foo}}", + "expErrors": [ + { + "type": "variant-key-mismatch" + } + ] + }, + { + "src": ".match {:foo} 1 {{_}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + { + "src": ".match {:foo} other {{_}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + { + "src": ".match {:foo} {:bar} * 1 {{_}} 1 * {{_}}", + "expErrors": [ + { + "type": "missing-fallback-variant" + } + ] + }, + { + "src": ".match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + { + "src": ".input {$foo} .match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + { + "src": ".local $foo = {$bar} .match {$foo} one {{one}} * {{other}}", + "expErrors": [ + { + "type": "missing-selector-annotation" + } + ] + }, + { + "src": ".input {$foo} .input {$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".input {$foo} .local $foo = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {42} .input {$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {:unknown} .local $foo = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$bar} .local $bar = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$bar} .local $bar = {$baz} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {$bar :func} .local $bar = {$baz} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {42 :func opt=$foo} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": ".local $foo = {42 :func opt=$bar} .local $bar = {42} {{_}}", + "expErrors": [ + { + "type": "duplicate-declaration" + } + ] + }, + { + "src": "bad {:placeholder option=x option=x}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + { + "src": "bad {:placeholder ns:option=x ns:option=y}", + "expErrors": [ + { + "type": "duplicate-option-name" + } + ] + }, + { + "src": ".match {$var :string} * {{The first default}} * {{The second default}}", + "expErrors": [ + { + "type": "duplicate-variant" + } + ] + }, + { + "src": ".match {$x :string} {$y :string} * foo {{The first foo variant}} bar * {{The bar variant}} * |foo| {{The second foo variant}} * * {{The default variant}}", + "expErrors": [ + { + "type": "duplicate-variant" + } + ] + } + ] } diff --git a/testdata/message2/spec/functions/date.json b/testdata/message2/spec/functions/date.json new file mode 100644 index 000000000000..dd14e6785fb6 --- /dev/null +++ b/testdata/message2/spec/functions/date.json @@ -0,0 +1,46 @@ +{ + "scenario": "Date function", + "description": "The built-in formatter for dates.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:date}", + "exp": "{:date}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{horse :date}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{|2006-01-02| :date}" + }, + { + "src": "{|2006-01-02T15:04:06| :date}" + }, + { + "src": "{|2006-01-02| :date style=long}" + }, + { + "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}" + }, + { + "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}", + "ignoreJava": "ICU4J doesn't support this kind of composition" + } + ] +} diff --git a/testdata/message2/spec/functions/datetime.json b/testdata/message2/spec/functions/datetime.json new file mode 100644 index 000000000000..bdfea3096cda --- /dev/null +++ b/testdata/message2/spec/functions/datetime.json @@ -0,0 +1,68 @@ +{ + "scenario": "Datetime function", + "description": "The built-in formatter for datetimes.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:datetime}", + "exp": "{:datetime}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{$x :datetime}", + "exp": "{$x}", + "params": [ + { + "name": "x", + "value": true + } + ], + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{horse :datetime}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime}" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}" + }, + { + "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}" + }, + { + "src": "{$dt :datetime}", + "params": [ + { + "type": "datetime", + "name": "dt", + "value": "2006-01-02T15:04:06" + } + ] + } + ] +} diff --git a/testdata/message2/spec/functions/integer.json b/testdata/message2/spec/functions/integer.json new file mode 100644 index 000000000000..c8e75077a221 --- /dev/null +++ b/testdata/message2/spec/functions/integer.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "scenario": "Integer function", + "description": "The built-in formatter for integers.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": "hello {4.2 :integer}", + "exp": "hello 4" + }, + { + "src": "hello {-4.20 :integer}", + "exp": "hello -4" + }, + { + "src": "hello {0.42e+1 :integer}", + "exp": "hello 4" + }, + { + "src": ".match {$foo :integer} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1.2 + } + ], + "exp": "one" + } + ] +} diff --git a/testdata/message2/spec/functions/number.json b/testdata/message2/spec/functions/number.json new file mode 100644 index 000000000000..1b81c705622b --- /dev/null +++ b/testdata/message2/spec/functions/number.json @@ -0,0 +1,407 @@ +{ + "scenario": "Number function", + "description": "The built-in formatter for numbers.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": "hello {4.2 :number}", + "exp": "hello 4.2" + }, + { + "src": "hello {-4.20 :number}", + "exp": "hello -4.2" + }, + { + "src": "hello {0.42e+1 :number}", + "exp": "hello 4.2" + }, + { + "src": "hello {foo :number}", + "exp": "hello {|foo|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|.1| :number}", + "exp": "invalid number literal {|.1|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|1.| :number}", + "exp": "invalid number literal {|1.|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|01| :number}", + "exp": "invalid number literal {|01|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|+1| :number}", + "exp": "invalid number literal {|+1|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "invalid number literal {|0x1| :number}", + "exp": "invalid number literal {|0x1|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "hello {:number}", + "exp": "hello {:number}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "hello {4.2 :number minimumFractionDigits=2}", + "exp": "hello 4.20" + }, + { + "src": "hello {|4.2| :number minimumFractionDigits=|2|}", + "exp": "hello 4.20" + }, + { + "src": "hello {4.2 :number minimumFractionDigits=$foo}", + "params": [ + { + "name": "foo", + "value": 2 + } + ], + "exp": "hello 4.20" + }, + { + "src": "hello {|4.2| :number minimumFractionDigits=$foo}", + "params": [ + { + "name": "foo", + "value": "2" + } + ], + "exp": "hello 4.20" + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": 4.2 + } + ], + "exp": "bar 4.2" + }, + { + "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": 4.2 + } + ], + "exp": "bar 4.20" + }, + { + "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": 4.2 + } + ], + "exp": "bar {$bar}", + "expErrors": [ + { + "type": "bad-option" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".local $foo = {$bar :number} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": "foo" + } + ], + "exp": "bar {$bar}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": 4.2 + } + ], + "exp": "bar 4.2" + }, + { + "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": 4.2 + } + ], + "exp": "bar 4.20" + }, + { + "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": 4.2 + } + ], + "exp": "bar {$foo}", + "expErrors": [ + { + "type": "bad-option" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".input {$foo :number} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar {$foo}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".match {$foo :number} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "=1" + }, + { + "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "=1" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + }, + { + "name": "bar", + "value": 1 + } + ], + "exp": "one one" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + }, + { + "name": "bar", + "value": 2 + } + ], + "exp": "one other" + }, + { + "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", + "params": [ + { + "name": "foo", + "value": 2 + }, + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", + "params": [ + { + "name": "bar", + "value": 2 + } + ], + "exp": "other" + }, + { + "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one" + }, + { + "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", + "params": [ + { + "name": "foo", + "value": 2 + } + ], + "exp": "{$none}", + "expErrors": [ + { + "type": "unresolved-variable" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{42 :number @foo @bar=13}", + "exp": "42", + "expParts": [ + { + "type": "number", + "source": "|42|", + "parts": [ + { + "type": "integer", + "value": "42" + } + ] + } + ] + } + ] +} diff --git a/testdata/message2/spec/functions/string.json b/testdata/message2/spec/functions/string.json new file mode 100644 index 000000000000..5858079b99a6 --- /dev/null +++ b/testdata/message2/spec/functions/string.json @@ -0,0 +1,51 @@ +{ + "scenario": "String function", + "description": "The built-in formatter for strings.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "src": ".match {$foo :string} |1| {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": "1" + } + ], + "exp": "one" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": 1 + } + ], + "exp": "one", + "ignoreJava": ":string doesn't stringify numbers?" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "params": [ + { + "name": "foo", + "value": null + } + ], + "exp": "other", + "ignoreCpp": "Can't handle null value for input variable" + }, + { + "src": ".match {$foo :string} 1 {{one}} * {{other}}", + "exp": "other", + "expErrors": [ + { + "type": "unresolved-variable" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + } + ] +} diff --git a/testdata/message2/spec/functions/time.json b/testdata/message2/spec/functions/time.json new file mode 100644 index 000000000000..845934a5e16a --- /dev/null +++ b/testdata/message2/spec/functions/time.json @@ -0,0 +1,43 @@ +{ + "scenario": "Time function", + "description": "The built-in formatter for times.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:time}", + "exp": "{:time}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{horse :time}", + "exp": "{|horse|}", + "expErrors": [ + { + "type": "bad-operand" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{|2006-01-02T15:04:06| :time}" + }, + { + "src": "{|2006-01-02T15:04:06| :time style=medium}" + }, + { + "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}" + }, + { + "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}", + "ignoreJava": "ICU4J doesn't support this kind of composition" + } + ] +} diff --git a/testdata/message2/spec/syntax-errors.json b/testdata/message2/spec/syntax-errors.json index fc4537131c83..34d9aa484572 100644 --- a/testdata/message2/spec/syntax-errors.json +++ b/testdata/message2/spec/syntax-errors.json @@ -1,56 +1,180 @@ -[ - ".", - "{", - "}", - "{}", - "{{", - "{{}", - "{{}}}", - "{|foo| #markup}", - "{{missing end brace}", - "{{missing end braces", - "{{missing end {$braces", - "{{extra}} content", - "empty { } placeholder", - "missing space {42:func}", - "missing space {|foo|:func}", - "missing space {|foo|@bar}", - "missing space {:func@bar}", - "missing space {:func @bar@baz}", - "missing space {:func @bar=42@baz}", - "missing space {+reserved@bar}", - "missing space {&private@bar}", - "bad {:} placeholder", - "bad {\\u0000placeholder}", - "no-equal {|42| :number minimumFractionDigits 2}", - "bad {:placeholder option=}", - "bad {:placeholder option value}", - "bad {:placeholder option:value}", - "bad {:placeholder option}", - "bad {:placeholder:}", - "bad {::placeholder}", - "bad {:placeholder::foo}", - "bad {:placeholder option:=x}", - "bad {:placeholder :option=x}", - "bad {:placeholder option::x=y}", - "bad {$placeholder option}", - "bad {:placeholder @attribute=}", - "bad {:placeholder @attribute=@foo}", - "no {placeholder end", - "no {$placeholder end", - "no {:placeholder end", - "no {|placeholder| end", - "no {|literal} end", - "no {|literal or placeholder end", - ".local bar = {|foo|} {{_}}", - ".local #bar = {|foo|} {{_}}", - ".local $bar {|foo|} {{_}}", - ".local $bar = |foo| {{_}}", - ".match {#foo} * {{foo}}", - ".match {} * {{foo}}", - ".match {|foo| :x} {|bar| :x} ** {{foo}}", - ".match * {{foo}}", - ".match {|x| :x} * foo", - ".match {|x| :x} * {{foo}} extra", - ".match |x| * {{foo}}" -] +{ + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "scenario": "Syntax errors", + "description": "Strings that produce syntax errors when parsed.", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ + { + "src": "." + }, + { + "src": "{" + }, + { + "src": "}" + }, + { + "src": "{}" + }, + { + "src": "{{" + }, + { + "src": "{{}" + }, + { + "src": "{{}}}" + }, + { + "src": "{|foo| #markup}" + }, + { + "src": "{{missing end brace}" + }, + { + "src": "{{missing end braces" + }, + { + "src": "{{missing end {$braces" + }, + { + "src": "{{extra}} content" + }, + { + "src": "empty { } placeholder" + }, + { + "src": "missing space {42:func}" + }, + { + "src": "missing space {|foo|:func}" + }, + { + "src": "missing space {|foo|@bar}" + }, + { + "src": "missing space {:func@bar}" + }, + { + "src": "missing space {:func @bar@baz}" + }, + { + "src": "missing space {:func @bar=42@baz}" + }, + { + "src": "missing space {+reserved@bar}" + }, + { + "src": "missing space {&private@bar}" + }, + { + "src": "bad {:} placeholder" + }, + { + "src": "bad {\\u0000placeholder}" + }, + { + "src": "no-equal {|42| :number minimumFractionDigits 2}" + }, + { + "src": "bad {:placeholder option=}" + }, + { + "src": "bad {:placeholder option value}" + }, + { + "src": "bad {:placeholder option:value}" + }, + { + "src": "bad {:placeholder option}" + }, + { + "src": "bad {:placeholder:}" + }, + { + "src": "bad {::placeholder}" + }, + { + "src": "bad {:placeholder::foo}" + }, + { + "src": "bad {:placeholder option:=x}" + }, + { + "src": "bad {:placeholder :option=x}" + }, + { + "src": "bad {:placeholder option::x=y}" + }, + { + "src": "bad {$placeholder option}" + }, + { + "src": "bad {:placeholder @attribute=}" + }, + { + "src": "bad {:placeholder @attribute=@foo}" + }, + { + "src": "{ @misplaced = attribute }" + }, + { + "src": "no {placeholder end" + }, + { + "src": "no {$placeholder end" + }, + { + "src": "no {:placeholder end" + }, + { + "src": "no {|placeholder| end" + }, + { + "src": "no {|literal} end" + }, + { + "src": "no {|literal or placeholder end" + }, + { + "src": ".local bar = {|foo|} {{_}}" + }, + { + "src": ".local #bar = {|foo|} {{_}}" + }, + { + "src": ".local $bar {|foo|} {{_}}" + }, + { + "src": ".local $bar = |foo| {{_}}" + }, + { + "src": ".match {#foo} * {{foo}}" + }, + { + "src": ".match {} * {{foo}}" + }, + { + "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}" + }, + { + "src": ".match * {{foo}}" + }, + { + "src": ".match {|x| :x} * foo" + }, + { + "src": ".match {|x| :x} * {{foo}} extra" + }, + { + "src": ".match |x| * {{foo}}" + } + ] +} diff --git a/testdata/message2/spec/syntax.json b/testdata/message2/spec/syntax.json new file mode 100644 index 000000000000..1b901eb639d7 --- /dev/null +++ b/testdata/message2/spec/syntax.json @@ -0,0 +1,970 @@ +{ + "$schema": "https://raw.githubusercontent.com/unicode-org/message-format-wg/main/test/schemas/v0/tests.schema.json", + "scenario": "Syntax", + "description": "Test cases that do not depend on any registry definitions.", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ + { + "description": "message -> simple-message -> ", + "src": "", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char", + "src": "a", + "exp": "a" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ...", + "src": "hello", + "exp": "hello" + }, + { + "description": "message -> simple-message -> simple-start pattern -> escaped-char", + "src": "\\\\", + "exp": "\\" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ... -> simple-start-char *text-char placeholder", + "src": "hello {world}", + "exp": "hello world" + }, + { + "description": "message -> simple-message -> simple-start pattern -> simple-start-char pattern -> ... -> simple-start-char *text-char placeholder", + "src": "hello {|world|}", + "exp": "hello world" + }, + { + "description": "message -> simple-message -> s simple-start pattern -> s simple-start-char pattern -> ...", + "src": "\n hello\t", + "exp": "\n hello\t" + }, + { + "src": "hello {$place}", + "params": [ + { + "name": "place", + "value": "world" + } + ], + "exp": "hello world" + }, + { + "src": "hello {$place-.}", + "params": [ + { + "name": "place-.", + "value": "world" + } + ], + "exp": "hello world" + }, + { + "src": "hello {$place}", + "expErrors": [ + { + "type": "unresolved-variable" + } + ], + "exp": "hello {$place}", + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> expression -> literal-expression -> \"{\" literal \"}\"", + "src": "{a}", + "exp": "a" + }, + { + "description": "... -> literal-expression -> \"{\" literal s annotation \"}\" -> \"{\" literal s function \"}\" -> \"{\" literal s \":\" identifier \"}\" -> \"{\" literal s \":\" name \"}\"", + "src": "{a :f}", + "exp": "{|a|}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> \"{\" literal s \":\" namespace \":\" name \"}\"", + "src": "{a :u:f}", + "exp": "{|a|}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> expression -> variable-expression -> \"{\" variable \"}\"", + "src": "{$x}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> variable-expression -> \"{\" variable s annotation \"}\" -> \"{\" variable s function \"}\" -> \"{\" variable s \":\" identifier \"}\" -> \"{\" variable s \":\" name \"}\"", + "src": "{$x :f}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }, { "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> \"{\" variable s \":\" namespace \":\" name \"}\"", + "src": "{$x :u:f}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }, { "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> annotation-expression -> function -> \"{\" \":\" namespace \":\" name \"}\"", + "src": "{:u:f}", + "exp": "{:u:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> annotation-expression -> function -> \"{\" \":\" name \"}\"", + "src": "{:f}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> complex-message -> complex-body -> quoted-pattern -> \"{{\" pattern \"}}\" -> \"{{\"\"}}\"", + "src": "{{}}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier \"}\"", + "src": "{#tag}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag" + } + ] + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> input-declaration complex-body -> input variable-expression complex-body", + "src": ".input{$x}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> s *(declaration [s]) complex-body s -> s declaration complex-body s -> s input-declaration complex-body s -> s input variable-expression complex-body s", + "src": "\t.input{$x}{{}}\n", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration declaration complex-body -> input-declaration input-declaration complex-body -> input variable-expression input variable-expression complex-body", + "src": ".input{$x}.input{$y}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration s declaration complex-body -> input-declaration s input-declaration complex-body -> input variable-expression s input variable-expression complex-body", + "src": ".input{$x} .input{$y}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> s *(declaration [s]) complex-body s -> s complex-body s", + "src": " {{}} ", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration declaration complex-body -> local-declaration input-declaration complex-body -> local s variable [s] \"=\" [s] expression input variable-expression complex-body", + "src": ".local $x ={a}.input{$y}{{}}", + "exp": "" + }, + { + "description": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> reserved-statement complex-body -> reserved-keyword expression -> \".\" name expression complex-body", + "src": ".n{a}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> complex-message -> complex-body -> matcher -> match-statement variant -> match selector key quoted-pattern -> \".match\" expression literal quoted-pattern", + "src": ".match{a :f}a{{}}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... input-declaration -> input s variable-expression ...", + "src": ".input {$x}{{}}", + "exp": "" + }, + { + "description": "... local-declaration -> local s variable s \"=\" expression ...", + "src": ".local $x ={a}{{}}", + "exp": "" + }, + { + "description": "... local-declaration -> local s variable \"=\" s expression ...", + "src": ".local $x= {a}{{}}", + "exp": "" + }, + { + "description": "... local-declaration -> local s variable s \"=\" expression ...", + "src": ".local $x = {a}{{}}", + "exp": "" + }, + { + "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector selector variant -> match selector selector variant key s key quoted-pattern", + "src": ".match{a :f}{b :f}a b{{}}* *{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... matcher -> match-statement [s] variant -> match 1*([s] selector) variant -> match selector variant variant ...", + "src": ".match{a :f}a{{}}b{{}}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... variant -> key s quoted-pattern -> ...", + "src": ".match{a :f}a {{}}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... variant -> key s key s quoted-pattern -> ...", + "src": ".match{a :f}{b :f}a b {{}}* *{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... key -> \"*\" ...", + "src": ".match{a :f}*{{}}", + "exp": "", + "expErrors": [ { "type": "unknown-function" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "simple-message -> simple-start pattern -> placeholder -> expression -> literal-expression -> \"{\" s literal \"}\"", + "src": "{ a}", + "exp": "a" + }, + { + "description": "... literal-expression -> \"{\" literal s attribute \"}\" -> \"{\" literal s \"@\" identifier \"}\"", + "src": "{a @c}", + "exp": "a" + }, + { + "description": "... -> literal-expression -> \"{\" literal s \"}\"", + "src": "{a }", + "exp": "a" + }, + { + "description": "simple-message -> simple-start pattern -> placeholder -> expression -> variable-expression -> \"{\" s variable \"}\"", + "src": "{ $x}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... variable-expression -> \"{\" variable s attribute \"}\" -> \"{\" variable s \"@\" identifier \"}\"", + "src": "{$x @c}", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> variable-expression -> \"{\" variable s \"}\"", + "src": "{$x }", + "exp": "{$x}", + "expErrors": [{ "type": "unresolved-variable" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "simple-message -> simple-start pattern -> placeholder -> expression -> annotation-expression -> \"{\" s annotation \"}\"", + "src": "{ :f}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... annotation-expression -> \"{\" annotation s attribute \"}\" -> \"{\" annotation s \"@\" identifier \"}\"", + "src": "{:f @c}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... -> annotation-expression -> \"{\" annotation s \"}\"", + "src": "{:f }", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... annotation -> private-use-annotation -> private-start", + "src": "{^}", + "exp": "{^}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... annotation -> reserved-annotation -> reserved-annotation-start", + "src": "{!}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"#\" identifier \"}\"", + "src": "{ #a}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier option \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#tag foo=bar}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier attribute \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#a @c}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier s \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#a }", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"#\" identifier \"/\" \"}\" -> \"{\" \"#\" identifier identifier \"=\" literal \"}\"", + "src": "{#a/}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier \"}\"", + "src": "{/a}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"/\" identifier \"}\"", + "src": "{ /a}", + "exp": "" + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier option \"}\"", + "src": "{/tag foo=bar}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "close", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "description": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" \"/\" identifier s \"}\"", + "src": "{/a }", + "exp": "" + }, + { + "description": "... annotation-expression -> function -> \":\" identifier option", + "src": "{:f k=v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... option -> identifier s \"=\" literal", + "src": "{:f k =v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... option -> identifier \"=\" s literal", + "src": "{:f k= v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... option -> identifier s \"=\" s literal", + "src": "{:f k = v}", + "exp": "{:f}", + "expErrors": [{ "type": "unknown-function" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... attribute -> \"@\" identifier \"=\" literal ...", + "src": "{a @c=d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier s \"=\" literal ...", + "src": "{a @c =d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier \"=\" s literal ...", + "src": "{a @c= d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier s \"=\" s literal ...", + "src": "{a @c = d}", + "exp": "a" + }, + { + "description": "... attribute -> \"@\" identifier s \"=\" s variable ...", + "src": "{42 @foo=$bar}", + "exp": "42", + "expParts": [ + { + "type": "string", + "source": "|42|", + "value": "42" + } + ] + }, + { + "description": "... literal -> quoted-literal -> \"|\" \"|\" ...", + "src": "{||}", + "exp": "" + }, + { + "description": "... quoted-literal -> \"|\" quoted-char \"|\"", + "src": "{|a|}", + "exp": "a" + }, + { + "description": "... quoted-literal -> \"|\" escaped-char \"|\"", + "src": "{|\\\\|}", + "exp": "\\" + }, + { + "description": "... quoted-literal -> \"|\" quoted-char escaped-char \"|\"", + "src": "{|a\\\\|}", + "exp": "a\\" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30", + "src": "{0}", + "exp": "0" + }, + { + "description": "... unquoted-literal -> number-literal -> \"-\" %x30", + "src": "{-0}", + "exp": "-0" + }, + { + "description": "... unquoted-literal -> number-literal -> (%x31-39 *DIGIT) -> %x31", + "src": "{1}", + "exp": "1" + }, + { + "description": "... unquoted-literal -> number-literal -> (%x31-39 *DIGIT) -> %x31 DIGIT -> 11", + "src": "{11}", + "exp": "11" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 \".\" 1*DIGIT -> 0 \".\" 1", + "src": "{0.1}", + "exp": "0.1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 \".\" 1*DIGIT -> %x30 \".\" DIGIT DIGIT -> 0 \".\" 1 2", + "src": "{0.12}", + "exp": "0.12" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" 1*DIGIT -> %x30 \"e\" DIGIT", + "src": "{0e1}", + "exp": "0e1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" 1*DIGIT -> %x30 \"E\" DIGIT", + "src": "{0E1}", + "exp": "0E1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" \"-\" 1*DIGIT ...", + "src": "{0E-1}", + "exp": "0E-1" + }, + { + "description": "... unquoted-literal -> number-literal -> %x30 %i\"e\" \"+\" 1*DIGIT ...", + "src": "{0E-1}", + "exp": "0E-1" + }, + { + "description": "... reserved-statement -> reserved-keyword s reserved-body 1*([s] expression) -> reserved-keyword s reserved-body expression -> \".\" name s reserved-body-part expression -> \".\" name s reserved-char expression ...", + "src": ".n .{a}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-statement -> reserved-keyword reserved-body 1*([s] expression) -> reserved-keyword s reserved-body s expression -> \".\" name s reserved-body-part expression -> \".\" name s reserved-char expression ...", + "src": ".n. {a}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-statement -> reserved-keyword reserved-body 1*([s] expression) -> reserved-keyword reserved-body expression expression -> \".\" name reserved-body-part expression expression -> \".\" name s reserved-char expression expression ...", + "src": ".n.{a}{b}{{}}", + "exp": "", + "expErrors": [ { "type": "unsupported-statement" } ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part -> \"!\" reserved-char ...", + "src": "{!.}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start s reserved-body -> \"!\" s reserved-body-part -> \"!\" s reserved-char ...", + "src": "{! .}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{%}", + "exp": "{%}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{*}", + "exp": "{*}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{+}", + "exp": "{+}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{<}", + "exp": "{<}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{>}", + "exp": "{>}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{?}", + "exp": "{?}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation-start ...", + "src": "{~}", + "exp": "{~}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... private-use-annotation -> private-start reserved-body -> \"^\" reserved-body-part -> \"^\" reserved-char ...", + "src": "{^.}", + "exp": "{^}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... private-use-annotation -> private-start s reserved-body -> \"^\" s reserved-body-part -> \"^\" s reserved-char ...", + "src": "{^ .}", + "exp": "{^}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... private-start ...", + "src": "{&}", + "exp": "{&}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part reserved-body-part -> \"!\" reserved-char escaped-char ...", + "src": "{!.\\{}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part s reserved-body-part -> \"!\" reserved-char s escaped-char ...", + "src": "{!. \\{}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "description": "... reserved-annotation -> reserved-annotation-start reserved-body -> \"!\" reserved-body-part -> \"!\" quoted-literal ...", + "src": "{!|a|}", + "exp": "{!}", + "expErrors": [{ "type": "unsupported-expression" }], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "hello { world\t\n}", + "exp": "hello world" + }, + { + "src": "hello {\u3000world\r}", + "exp": "hello world" + }, + { + "src": "{$one} and {$two}", + "params": [ + { + "name": "one", + "value": 1.3 + }, + { + "name": "two", + "value": 4.2 + } + ], + "exp": "1.3 and 4.2" + }, + { + "src": "{$one} et {$two}", + "locale": "fr", + "params": [ + { + "name": "one", + "value": 1.3 + }, + { + "name": "two", + "value": 4.2 + } + ], + "exp": "1,3 et 4,2" + }, + { + "src": ".local $foo = {bar} {{bar {$foo}}}", + "exp": "bar bar" + }, + { + "src": ".local $foo = {|bar|} {{bar {$foo}}}", + "exp": "bar bar" + }, + { + "src": ".local $foo = {|bar|} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar bar" + }, + { + "src": ".local $foo = {$bar} {{bar {$foo}}}", + "params": [ + { + "name": "bar", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": [ + { + "name": "baz", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".input {$foo} {{bar {$foo}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", + "params": [ + { + "name": "foo", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", + "params": [ + { + "name": "baz", + "value": "foo" + } + ], + "exp": "bar foo" + }, + { + "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", + "exp": "42 42" + }, + { + "src": "{#tag}content", + "exp": "content", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag" + }, + { + "type": "literal", + "value": "content" + } + ] + }, + { + "src": "{#ns:tag}content{/ns:tag}", + "exp": "content", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "ns:tag" + }, + { + "type": "literal", + "value": "content" + }, + { + "type": "markup", + "kind": "close", + "name": "ns:tag" + } + ] + }, + { + "src": "{/tag}content", + "exp": "content", + "expParts": [ + { + "type": "markup", + "kind": "close", + "name": "tag" + }, + { + "type": "literal", + "value": "content" + } + ] + }, + { + "src": "{#tag foo=bar/}", + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "standalone", + "name": "tag", + "options": { + "foo": "bar" + } + } + ] + }, + { + "src": "{#tag a:foo=|foo| b:bar=$bar}", + "params": [ + { + "name": "bar", + "value": "b a r" + } + ], + "exp": "", + "expParts": [ + { + "type": "markup", + "kind": "open", + "name": "tag", + "options": { + "a:foo": "foo", + "b:bar": "b a r" + } + } + ] + }, + { + "src": "{42 @foo @bar=13}", + "exp": "42", + "expParts": [ + { + "type": "string", + "source": "|42|", + "value": "42" + } + ] + }, + { + "src": "foo {+reserved}", + "exp": "foo {+}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "+" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "foo {&private}", + "exp": "foo {&}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "&" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "foo {?reserved @a @b=$c}", + "exp": "foo {?}", + "expParts": [ + { + "type": "literal", + "value": "foo " + }, + { + "type": "fallback", + "source": "?" + } + ], + "expErrors": [ + { + "type": "unsupported-expression" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".foo {42} {{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".foo{42}{{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".foo |}lit{| {42}{{bar}}", + "exp": "bar", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": ".l $y = {|bar|} {{}}", + "exp": "", + "expParts": [ + { + "type": "literal", + "value": "bar" + } + ], + "expErrors": [ + { + "type": "unsupported-statement" + } + ], + "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" + }, + { + "src": "{{trailing whitespace}} \n", + "exp": "trailing whitespace" + } + ] +} diff --git a/testdata/message2/spec/test-core.json b/testdata/message2/spec/test-core.json deleted file mode 100644 index 2ee0786e62de..000000000000 --- a/testdata/message2/spec/test-core.json +++ /dev/null @@ -1,219 +0,0 @@ -[ - { "src": "hello", "exp": "hello" }, - { "src": "hello {world}", "exp": "hello world" }, - { - "src": "hello { world\t\n}", - "exp": "hello world", - "cleanSrc": "hello {world}" - }, - { - "src": "hello {\u3000world\r}", - "exp": "hello world", - "cleanSrc": "hello {world}" - }, - { "src": "hello {|world|}", "exp": "hello world" }, - { "src": "hello {||}", "exp": "hello " }, - { - "src": "hello {$place}", - "params": { "place": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place-.}", - "params": { "place-.": "world" }, - "exp": "hello world" - }, - { - "src": "hello {$place}", - "errors": [{ "type": "unresolved-var" }], - "exp": "hello {$place}", - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{$one} and {$two}", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1.3 and 4.2" - }, - { - "src": "{$one} et {$two}", - "locale": "fr", - "params": { "one": 1.3, "two": 4.2 }, - "exp": "1,3 et 4,2" - }, - { "src": ".local $foo = {bar} {{bar {$foo}}}", "exp": "bar bar" }, - { "src": ".local $foo = {|bar|} {{bar {$foo}}}", "exp": "bar bar" }, - { - "src": ".local $foo = {|bar|} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar bar" - }, - { - "src": ".local $foo = {$bar} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar foo" - }, - { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar foo" - }, - { - "src": ".input {$foo} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "foo": "foo" }, - "exp": "bar foo" - }, - { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" - }, - { "src": ".local $x = {42} .local $y = {$x} {{{$x} {$y}}}", "exp": "42 42" }, - { - "src": "{#tag}", - "exp": "", - "parts": [{ "type": "markup", "kind": "open", "name": "tag" }] - }, - { - "src": "{#tag}content", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "open", "name": "tag" }, - { "type": "literal", "value": "content" } - ] - }, - { - "src": "{#ns:tag}content{/ns:tag}", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "open", "name": "ns:tag" }, - { "type": "literal", "value": "content" }, - { "type": "markup", "kind": "close", "name": "ns:tag" } - ] - }, - { - "src": "{/tag}content", - "exp": "content", - "parts": [ - { "type": "markup", "kind": "close", "name": "tag" }, - { "type": "literal", "value": "content" } - ] - }, - { - "src": "{#tag foo=bar}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "open", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{#tag foo=bar/}", - "cleanSrc": "{#tag foo=bar /}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "standalone", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{#tag a:foo=|foo| b:bar=$bar}", - "params": { "bar": "b a r" }, - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "open", - "name": "tag", - "options": { "a:foo": "foo", "b:bar": "b a r" } - } - ] - }, - { - "src": "{/tag foo=bar}", - "exp": "", - "parts": [ - { - "type": "markup", - "kind": "close", - "name": "tag", - "options": { "foo": "bar" } - } - ] - }, - { - "src": "{42 @foo @bar=13}", - "exp": "42", - "parts": [{ "type": "string", "value": "42" }] - }, - { - "src": "{42 @foo=$bar}", - "exp": "42", - "parts": [{ "type": "string", "value": "42" }] - }, - { - "src": "foo {+reserved}", - "exp": "foo {+}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "+" } - ], - "errors": [{ "type": "unsupported-annotation" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "foo {&private}", - "exp": "foo {&}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "&" } - ], - "errors": [{ "type": "unsupported-annotation" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "foo {?reserved @a @b=$c}", - "exp": "foo {?}", - "parts": [ - { "type": "literal", "value": "foo " }, - { "type": "fallback", "source": "?" } - ], - "errors": [{ "type": "unsupported-annotation" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".foo {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".foo{42}{{bar}}", - "cleanSrc": ".foo {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".foo |}lit{| {42}{{bar}}", - "cleanSrc": ".foo |}lit{| {42} {{bar}}", - "exp": "bar", - "parts": [{ "type": "literal", "value": "bar" }], - "errors": [{ "type": "unsupported-statement" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } -] diff --git a/testdata/message2/spec/test-functions.json b/testdata/message2/spec/test-functions.json deleted file mode 100644 index a8979106947e..000000000000 --- a/testdata/message2/spec/test-functions.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "date": [ - { "src": "{:date}", - "exp": "{:date}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{horse :date}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }], - "errorsJs": [{ "name": "RangeError" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { "src": "{|2006-01-02| :date}", "exp": "1/2/06" }, - { "src": "{|2006-01-02T15:04:06| :date}", "exp": "1/2/06" }, - { "src": "{|2006-01-02| :date style=long}", "exp": "January 2, 2006" }, - { - "src": ".local $d = {|2006-01-02| :date style=long} {{{$d :date}}}", - "exp": "January 2, 2006" - }, - { - "ignoreJava": "Can't chain :time and :date, they are different types", - "src": ".local $t = {|2006-01-02T15:04:06| :time} {{{$t :date}}}", - "exp": "1/2/06" - } - ], - "time": [ - { "src": "{:time}", "exp": "{:time}", "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{horse :time}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { "src": "{|2006-01-02T15:04:06| :time}", "expJs": "3:04 PM", "exp": "3:04 PM" }, - { - "src": "{|2006-01-02T15:04:06| :time style=medium}", - "expJs": "3:04:06 PM", - "exp": "3:04:06 PM" - }, - { - "src": ".local $t = {|2006-01-02T15:04:06| :time style=medium} {{{$t :time}}}", - "expJs": "3:04:06 PM", - "exp": "3:04:06 PM" - }, - { - "ignoreJava": "Can't chain :time and :date, they are different types", - "src": ".local $d = {|2006-01-02T15:04:06| :date} {{{$d :time}}}", - "exp": "3:04 PM" - } - ], - "datetime": [ - { - "src": "{:datetime}", - "exp": "{:datetime}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "{$x :datetime}", - "exp": "{$x}", - "params": { "x": true }, - "errors": [{ "type": "bad-input" }] - }, - { - "src": "{horse :datetime}", - "exp": "{|horse|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { "src": "{|2006-01-02T15:04:06| :datetime}", "expJs": "1/2/06, 3:04 PM", "exp": "1/2/06, 3:04 PM" }, - { - "src": "{|2006-01-02T15:04:06| :datetime year=numeric month=|2-digit|}", - "exp": "01/2006" - }, - { - "src": "{|2006-01-02T15:04:06| :datetime dateStyle=long}", - "exp": "January 2, 2006" - }, - { - "src": "{|2006-01-02T15:04:06| :datetime timeStyle=medium}", - "expJs": "3:04:06 PM", - "exp": "3:04:06 PM" - }, - { - "src": "{$dt :datetime}", - "params": { "dt": "2006-01-02T15:04:06" }, - "expJs": "1/2/06, 3:04 PM", - "exp": "1/2/06, 3:04 PM" - }, - { - "ignoreJava": "Can't chain :time and :date, they are different types", - "ignoreCpp": "Same reason as Java", - "src": ".input {$dt :time style=medium} {{{$dt :datetime dateStyle=long}}}", - "params": { "dt": "2006-01-02T15:04:06" }, - "exp": "January 2, 2006 at 3:04:06 PM" - } - ], - "integer": [ - { "src": "hello {4.2 :integer}", "exp": "hello 4" }, - { "src": "hello {-4.20 :integer}", "exp": "hello -4" }, - { "src": "hello {0.42e+1 :integer}", "exp": "hello 4" }, - { - "src": ".match {$foo :integer} one {{one}} * {{other}}", - "params": { "foo": 1.2 }, - "exp": "one" - } - ], - "number": [ - { "src": "hello {4.2 :number}", "exp": "hello 4.2" }, - { "src": "hello {-4.20 :number}", "exp": "hello -4.2" }, - { "src": "hello {0.42e+1 :number}", "exp": "hello 4.2" }, - { - "src": "hello {foo :number}", - "exp": "hello {|foo|}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {:number}", - "exp": "hello {:number}", - "errors": [{ "type": "bad-input" }] - }, - { - "src": "hello {4.2 :number minimumFractionDigits=2}", - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=|2|}", - "exp": "hello 4.20" - }, - { - "src": "hello {4.2 :number minimumFractionDigits=$foo}", - "params": { "foo": 2 }, - "exp": "hello 4.20" - }, - { - "src": "hello {|4.2| :number minimumFractionDigits=$foo}", - "params": { "foo": "2" }, - "exp": "hello 4.20" - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.2" - }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "exp": "bar 4.20" - }, - { - "src": ".local $foo = {$bar :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "bar": 4.2 }, - "comment": "I think it is fine to ignore invalid options", - "expJs": "bar {$bar}", - "exp": "bar 4.2", - "errorsJs": [{ "type": "bad-option" }] - }, - { - "src": ".local $foo = {$bar :number} {{bar {$foo}}}", - "params": { "bar": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar 4.2" - }, - { - "src": ".input {$foo :number minimumFractionDigits=2} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "exp": "bar 4.20" - }, - { - "src": ".input {$foo :number minimumFractionDigits=foo} {{bar {$foo}}}", - "params": { "foo": 4.2 }, - "comment": "I think it is fine to ignore invalid options", - "exp": "bar 4.2", - "expJs": "bar {$foo}", - "errorsJs": [{ "type": "bad-option" }] - }, - { - "src": ".input {$foo :number} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar {$foo}", - "errors": [{ "type": "bad-input" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": ".match {$foo :number} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".match {$foo :number} 1 {{=1}} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "src": ".match {$foo :number} one {{one}} 1 {{=1}} * {{other}}", - "params": { "foo": 1 }, - "exp": "=1" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 1 }, - "exp": "one one" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 1, "bar": 2 }, - "exp": "one other" - }, - { - "src": ".match {$foo :number} {$bar :number} one one {{one one}} one * {{one other}} * * {{other}}", - "params": { "foo": 2, "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$foo :number} .match {$foo} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $foo = {$bar :number} .match {$foo} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$foo :number} .local $bar = {$foo} .match {$bar} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar :number} .match {$bar} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 1 }, - "exp": "one" - }, - { - "src": ".input {$bar} .match {$bar :number} one {{one}} * {{other}}", - "params": { "bar": 2 }, - "exp": "other" - }, - { - "src": ".input {$none} .match {$foo :number} one {{one}} * {{{$none}}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 1 }, - "exp": "one" - }, - { - "src": ".local $bar = {$none} .match {$foo :number} one {{one}} * {{{$bar}}}", - "params": { "foo": 2 }, - "exp": "{$none}", - "errors": [{ "type": "unresolved-var" }] - }, - { - "src": "{42 :number @foo @bar=13}", - "exp": "42", - "parts": [ - { "type": "number", "parts": [{ "type": "integer", "value": "42" }] } - ] - } - ], - "ordinal": [ - { - "src": ".match {$foo :ordinal} one {{st}} two {{nd}} few {{rd}} * {{th}}", - "params": { "foo": 1 }, - "exp": "th", - "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {42 :ordinal}", - "exp": "hello {|42|}", - "errors": [{ "type": "missing-func" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } - ], - "plural": [ - { - "src": ".match {$foo :plural} one {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "other", - "errors": [{ "type": "missing-func" }, { "type": "not-selectable" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - }, - { - "src": "hello {42 :plural}", - "exp": "hello {|42|}", - "errors": [{ "type": "missing-func" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } - ], - "string": [ - { - "src": ".match {$foo :string} |1| {{one}} * {{other}}", - "params": { "foo": "1" }, - "exp": "one" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": 1 }, - "exp": "one", - "ignoreJava": "See https://unicode-org.atlassian.net/browse/ICU-22754?focusedCommentId=175933" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "params": { "foo": null }, - "exp": "other" - }, - { - "src": ".match {$foo :string} 1 {{one}} * {{other}}", - "exp": "other", - "errors": [{ "type": "unresolved-var" }], - "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" - } - ] -} diff --git a/testdata/message2/spec/unsupported-expressions.json b/testdata/message2/spec/unsupported-expressions.json new file mode 100644 index 000000000000..f7d611509d23 --- /dev/null +++ b/testdata/message2/spec/unsupported-expressions.json @@ -0,0 +1,53 @@ +{ + "scenario": "Reserved and private annotations", + "description": "Tests for unsupported expressions (reserved/private)", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "unsupported-expression" + } + ] + }, + "tests": [ + { "src": "hello {|4.2| %number}" }, + { "src": "hello {|4.2| %n|um|ber}" }, + { "src": "{+42}" }, + { "src": "hello {|4.2| &num|be|r}" }, + { "src": "hello {|4.2| ^num|be|r}" }, + { "src": "hello {|4.2| +num|be|r}" }, + { "src": "hello {|4.2| ?num|be||r|s}" }, + { "src": "hello {|foo| !number}" }, + { "src": "hello {|foo| *number}" }, + { "src": "hello {?number}" }, + { "src": "{xyzz }" }, + { "src": "hello {$foo ~xyzz }" }, + { "src": "hello {$x xyzz }" }, + { "src": "{ !xyzz }" }, + { "src": "{~xyzz }" }, + { "src": "{ num x \\\\ abcde |aaa||3.14||42| r }" }, + { "src": "hello {$foo >num x \\\\ abcde |aaa||3.14| |42| r }" }, + { "src" : ".input{ $n ~ }{{{$n}}}" } + ] +} + diff --git a/testdata/message2/spec/unsupported-statements.json b/testdata/message2/spec/unsupported-statements.json new file mode 100644 index 000000000000..d944aa0f786d --- /dev/null +++ b/testdata/message2/spec/unsupported-statements.json @@ -0,0 +1,18 @@ +{ + "scenario": "Reserved statements", + "description": "Tests for unsupported statements", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "unsupported-statement" + } + ] + }, + "tests": [ + { "src" : ".i {1} {{}}" }, + { "src" : ".l $y = {|bar|} {{}}" }, + { "src" : ".l $x.y = {|bar|} {{}}" } + ] +} + diff --git a/testdata/message2/syntax-errors-diagnostics-multiline.json b/testdata/message2/syntax-errors-diagnostics-multiline.json index b7b87036f20a..facfadebab47 100644 --- a/testdata/message2/syntax-errors-diagnostics-multiline.json +++ b/testdata/message2/syntax-errors-diagnostics-multiline.json @@ -1,4 +1,15 @@ -[ +{ + "scenario": "Syntax errors with character and line offsets", + "description": "Syntax errors; for ICU4C, the character and line offsets in the parse error are checked", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ { "src": "{{hello wo\nrld", "char": 3, @@ -35,4 +46,5 @@ "line": 3, "comment": "Offset for end-of-input should be 0 here because the line begins after the '\n', but there is no character after the '\n'" } -] + ] +} diff --git a/testdata/message2/syntax-errors-diagnostics.json b/testdata/message2/syntax-errors-diagnostics.json index 2b0188f6b557..5b68ae80f31b 100644 --- a/testdata/message2/syntax-errors-diagnostics.json +++ b/testdata/message2/syntax-errors-diagnostics.json @@ -1,402 +1,348 @@ -[ - { "src": "}{|xyz|", "char": 0, "errors": [{"type": "parse-error"}] }, - { "src": "}", "char": 0, "errors": [{"type": "parse-error"}] }, +{ + "scenario": "Syntax errors with character offsets", + "description": "Syntax errors; for ICU4C, the character offset in the parse error is checked", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ + { "src": "}{|xyz|", "char": 0 }, + { "src": "}", "char": 0 }, { "src": "{{{%\\y{}}", "char": 5, - "comment": "Backslash followed by non-backslash followed by a '{' -- this should be an error immediately after the first backslash", - "errors": [{"type": "parse-error"}] + "comment": "Backslash followed by non-backslash followed by a '{' -- this should be an error immediately after the first backslash" }, { "src": "{%abc|\\z}}", "char": 7, - "comment": "Reserved chars followed by a '|' that doesn't begin a valid literal -- this should be an error at the first invalid char in the literal", - "errors": [{"type": "parse-error"}] + "comment": "Reserved chars followed by a '|' that doesn't begin a valid literal -- this should be an error at the first invalid char in the literal" }, { "src": "{%\\y{p}}", "char": 3, - "comment": "Same pattern, but with a valid reserved-char following the erroneous reserved-escape -- the offset should be the same as with the previous one", - "errors": [{"type": "parse-error"}] + "comment": "Same pattern, but with a valid reserved-char following the erroneous reserved-escape -- the offset should be the same as with the previous one" }, { "src": "{{{%ab|\\z|cd}}", "char": 8, - "comment": "Erroneous literal inside a reserved string -- the error should be at the first erroneous literal char", - "errors": [{"type": "parse-error"}] + "comment": "Erroneous literal inside a reserved string -- the error should be at the first erroneous literal char" }, { "src": "hello {|4.2| %num\\ber}}", "char": 18, - "comment": "Single backslash not allowed", - "errors": [{"type": "parse-error"}] + "comment": "Single backslash not allowed" }, { "src": "hello {|4.2| %num{be\\|r}}", "char": 17, - "comment": "Unescaped '{' not allowed", - "errors": [{"type": "parse-error"}] + "comment": "Unescaped '{' not allowed" }, { "src": "hello {|4.2| %num}be\\|r}}", - "char": 21, - "comment": "Unescaped '}' -- will be interpreted as the end of the reserved string, and the error will be reported at the index of '|', which is when the parser determines that the escaped '|' isn't a valid text-escape", - "errors": [{"type": "parse-error"}] - }, + "char": 23, + "comment": "Unescaped '}' -- will be interpreted as the end of the reserved string, and the error will be reported at the index of the next '}'" }, { "src": "hello {|4.2| %num\\{be|r}}", "char": 25, - "comment": "Unescaped '|' -- will be interpreted as the beginning of a literal. Error at end of input", - "errors": [{"type": "parse-error"}] + "comment": "Unescaped '|' -- will be interpreted as the beginning of a literal. Error at end of input" }, { "src": ".match{|y|}|y|{{{|||}}}", "char": 19, - "comment": "No spaces are required here. The error should be in the pattern, not before", - "errors": [{"type": "parse-error"}] + "comment": "No spaces are required here. The error should be in the pattern, not before" }, { "src": ".match {|y|}|foo|bar {{{a}}}", "char": 17, - "comment": "Missing spaces between keys", - "errors": [{"type": "parse-error"}] + "comment": "Missing spaces between keys" }, { "src": ".match {|y|} |quux| |foo|bar {{{a}}}", "char": 25, - "comment": "Missing spaces between keys", - "errors": [{"type": "parse-error"}] + "comment": "Missing spaces between keys" }, { "src": ".match {|y|} |quux| |foo||bar| {{{a}}}", "char": 26, - "comment": "Missing spaces between keys", - "errors": [{"type": "parse-error"}] + "comment": "Missing spaces between keys" }, { "src": ".match {|y|} |\\q| * %{! {z}", "char": 16, - "comment": "Error parsing the first key -- the error should be there, not in the also-erroneous third key", - "errors": [{"type": "parse-error"}] + "comment": "Error parsing the first key -- the error should be there, not in the also-erroneous third key" }, { "src": ".match {|y|} * %{! {z} |\\q|", "char": 16, - "comment": "Error parsing the second key -- the error should be there, not in the also-erroneous third key", - "errors": [{"type": "parse-error"}] + "comment": "Error parsing the second key -- the error should be there, not in the also-erroneous third key" }, { "src": ".match {|y|} * |\\q| {\\z}", "char": 18, - "comment": "Error parsing the last key -- the error should be there, not in the erroneous pattern", - "errors": [{"type": "parse-error"}] + "comment": "Error parsing the last key -- the error should be there, not in the erroneous pattern" }, { "src": ".match {|y|} {\\|} {@} * * * {{a}}", "char": 14, - "comment": "Non-expression as scrutinee in pattern -- error should be at the first non-expression, not the later non-expression", - "errors": [{"type": "parse-error"}] + "comment": "Non-expression as scrutinee in pattern -- error should be at the first non-expression, not the later non-expression" }, { "src": ".match {|y|} $foo * {{a}} when * :bar {{b}}", "char": 14, - "comment": "Non-key in variant -- error should be there, not in the next erroneous variant", - "errors": [{"type": "parse-error"}] + "comment": "Non-key in variant -- error should be there, not in the next erroneous variant" }, { "src": "{{ foo {|bar|} \\q baz ", "char": 16, - "comment": "Error should be within the first erroneous `text` or expression", - "errors": [{"type": "parse-error"}] + "comment": "Error should be within the first erroneous `text` or expression" }, { "src": "{{{: }}}", "char": 4, - "comment": "':' has to be followed by a function name -- the error should be at the first whitespace character", - "errors": [{"type": "parse-error"}] + "comment": "':' has to be followed by a function name -- the error should be at the first whitespace character" }, { "src": ".local $x = }|foo|}", "char": 12, - "comment": "Expression not starting with a '{'", - "errors": [{"type": "parse-error"}] - }, - { - "src": ".local $x = {|foo|} .l $y = {|bar|} .local $z {|quux|}", - "char": 22, - "comment": "Error should be at the first declaration not starting with a `.local`", - "errors": [{"type": "parse-error"}] + "comment": "Expression not starting with a '{'" }, { "src": ".local $bar {|foo|} {{$bar}}", "char": 12, - "comment": "Missing '=' in `.local` declaration", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' in `.local` declaration" }, { "src": ".local bar = {|foo|} {{$bar}}", "char": 7, - "comment": "LHS of declaration doesn't start with a '$'", - "errors": [{"type": "parse-error"}] + "comment": "LHS of declaration doesn't start with a '$'" }, { "src": ".local $bar = |foo| {{$bar}}", "char": 14, - "comment": "`.local` RHS isn't an expression", - "errors": [{"type": "parse-error"}] + "comment": "`.local` RHS isn't an expression" }, { "src": "{{extra}}content", "char": 9, - "comment": "Trailing characters that are not whitespace", - "errors": [{"type": "parse-error"}] + "comment": "Trailing characters that are not whitespace" }, { "src": ".match {|x|} * {{foo}}extra", "char": 28, - "comment": "Trailing characters that are not whitespace", - "errors": [{"type": "parse-error"}] + "comment": "Trailing characters that are not whitespace" }, { "src": ".match {$foo :string} {$bar :string} one * {{one}} * * {{other}} ", "char": 66, - "comment": "Trailing whitespace at end of message should not be accepted either", - "errors": [{"type": "parse-error"}] - }, - { - "src": "{{hi}} ", - "char": 6, - "comment": "Trailing whitespace at end of message should not be accepted either", - "errors": [{"type": "parse-error"}] + "comment": "Trailing whitespace at end of message should not be accepted either" }, { "src": "empty { }", "char": 8, - "comment": "Empty expression", - "errors": [{"type": "parse-error"}] + "comment": "Empty expression" }, { "src": ".match {} * {{foo}}", "char": 8, - "comment": "Empty expression", - "errors": [{"type": "parse-error"}] + "comment": "Empty expression" }, { "src": "bad {:}", "char": 6, - "comment": "':' not preceding a function name", - "errors": [{"type": "parse-error"}] + "comment": "':' not preceding a function name" }, { "src": "{{no-equal {|42| :number m }}}", "char": 27, - "comment": "Missing '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' after option name" }, { "src": "{{no-equal {|42| :number minimumFractionDigits 2}}}", "char": 47, - "comment": "Missing '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' after option name" }, { "src": "bad {:placeholder option value}", "char": 25, - "comment": "Missing '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Missing '=' after option name" }, { "src": "hello {|4.2| :number min=2=3}", "char": 26, - "comment": "Extra '=' after option name", - "errors": [{"type": "parse-error"}] + "comment": "Extra '=' after option name" }, { "src": "hello {|4.2| :number min=2max=3}", "char": 26, - "comment": "Missing space between options", - "errors": [{"type": "parse-error"}] + "comment": "Missing space between options" }, { "src": "hello {|4.2| :number min=|a|max=3}", "char": 28, - "comment": "Missing whitespace between valid options", - "errors": [{"type": "parse-error"}] + "comment": "Missing whitespace between valid options" }, { "src": "hello {|4.2| :number min=|\\a|}", "char": 27, - "comment": "Ill-formed RHS of option -- the error should be within the RHS, not after parsing options", - "errors": [{"type": "parse-error"}] + "comment": "Ill-formed RHS of option -- the error should be within the RHS, not after parsing options" }, { "src": "no-equal {|42| :number {}", "char": 25, - "comment": "Junk after annotation", - "errors": [{"type": "parse-error"}] + "comment": "Junk after annotation" }, { "src": "bad {:placeholder option=}", "char": 25, - "comment": "Missing RHS of option", - "errors": [{"type": "parse-error"}] + "comment": "Missing RHS of option" }, { "src": "bad {:placeholder option}", "char": 24, - "comment": "Missing RHS of option", - "errors": [{"type": "parse-error"}] + "comment": "Missing RHS of option" }, { "src": "bad {$placeholder option}", "char": 18, - "comment": "Annotation is not a function or reserved text", - "errors": [{"type": "parse-error"}] + "comment": "Annotation is not a function or reserved text" }, { "src": "no {$placeholder end", "char": 17, - "comment": "Annotation is not a function or reserved text", - "errors": [{"type": "parse-error"}] + "comment": "Annotation is not a function or reserved text" }, { "src": ".match * {{foo}}", "char": 8, - "comment": "Missing expression in selectors", - "errors": [{"type": "parse-error"}] + "comment": "Missing expression in selectors" }, { "src": ".match |x| * {{foo}}", "char": 7, - "comment": "Non-expression in selectors", - "errors": [{"type": "parse-error"}] + "comment": "Non-expression in selectors" }, { "src": ".match {|x|} * foo", "char": 19, - "comment": "Missing RHS in variant", - "errors": [{"type": "parse-error"}] + "comment": "Missing RHS in variant" }, { "src": "{$:abc}", "char": 2, - "comment": "Variable names can't start with a : or -", - "errors": [{"type": "parse-error"}] + "comment": "Variable names can't start with a : or -" }, { "src": "{$-abc}", "char": 2, - "comment": "Variable names can't start with a : or -", - "errors": [{"type": "parse-error"}] + "comment": "Variable names can't start with a : or -" }, { "src": "{$bar+foo}", "char": 5, - "comment": "Missing space before annotation. Note that {{$bar:foo}} and {{$bar-foo}} are valid, because variable names can contain a ':' or a '-'", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation. Note that {{$bar:foo}} and {{$bar-foo}} are valid, because variable names can contain a ':' or a '-'" }, { "src": "{|3.14|:foo}", "char": 7, - "comment": "Missing space before annotation.", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation." }, { "src": "{|3.14|-foo}", "char": 7, - "comment": "Missing space before annotation.", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation." }, { "src": "{|3.14|+foo}", "char": 7, - "comment": "Missing space before annotation.", - "errors": [{"type": "parse-error"}] + "comment": "Missing space before annotation." }, { "src": ".local $foo = {$bar} .match {$foo} :one {one} * {other}", "char": 36, - "comment": "Unquoted literals can't begin with a ':'", - "errors": [{"type": "parse-error"}] + "comment": "Unquoted literals can't begin with a ':'" }, { "src": ".local $foo = {$bar :fun option=:a} {{bar {$foo}}}", "char": 32, - "comment": "Unquoted literals can't begin with a ':'", - "errors": [{"type": "parse-error"}] - }, - { "src": "{|foo| {#markup}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": "{|foo| #markup}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": "{|foo| {#markup/}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": "{|foo| {/markup}}", "char": 7, "comment": "Markup in wrong place", "errors": [{"type": "parse-error"}] }, - { "src": ".input $x = {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input $x = {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input {|1| :number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".input {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression", "errors": [{"type": "parse-error"}] }, - { "src": ".", "char": 1, "errors": [{"type": "parse-error"}]}, - { "src": "{", "char": 1, "errors": [{"type": "parse-error"}]}, - { "src": "}", "char": 0, "errors": [{"type": "parse-error"}]}, - { "src": "{}", "char": 1, "errors": [{"type": "parse-error"}]}, - { "src": "{{", "char": 2, "errors": [{"type": "parse-error"}]}, - { "src": "{{}", "char": 3, "errors": [{"type": "parse-error"}]}, - { "src": "{{}}}", "char": 4, "errors": [{"type": "parse-error"}]}, - { "src": "{|foo| #markup}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": "{{missing end brace}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "{{missing end braces", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "{{missing end {$braces", "char": 22, "errors": [{"type": "parse-error"}]}, - { "src": "{{extra}} content", "char": 9, "errors": [{"type": "parse-error"}]}, - { "src": "empty { } placeholder", "char": 8, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {42:func}", "char": 17, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {|foo|:func}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {|foo|@bar}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "missing space {:func@bar}", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "{:func @bar@baz}", "char": 11, "errors": [{"type": "parse-error"}]}, - { "src": "{:func @bar=42@baz}", "char": 14, "errors": [{"type": "parse-error"}]}, - { "src": "{+reserved@bar}", "char": 10, "errors": [{"type": "parse-error"}]}, - { "src": "{&private@bar}", "char": 9, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:} placeholder", "char": 6, "errors": [{"type": "parse-error"}]}, - { "src": "bad {\\u0000placeholder}", "char": 5, "errors": [{"type": "parse-error"}]}, - { "src": "no-equal {|42| :number minimumFractionDigits 2}", "char": 45, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option=}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option value}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option:value}", "char": 30, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option}", "char": 24, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder:}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {::placeholder}", "char": 6, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder::foo}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option:=x}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder :option=x}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder option::x=y}", "char": 25, "errors": [{"type": "parse-error"}]}, - { "src": "bad {$placeholder option}", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder @attribute=}", "char": 29, "errors": [{"type": "parse-error"}]}, - { "src": "bad {:placeholder @attribute=@foo}", "char": 29, "errors": [{"type": "parse-error"}]}, - { "src": "no {placeholder end", "char": 16, "errors": [{"type": "parse-error"}]}, - { "src": "no {$placeholder end", "char": 17, "errors": [{"type": "parse-error"}]}, - { "src": "no {:placeholder end", "char": 20, "errors": [{"type": "parse-error"}]}, - { "src": "no {|placeholder| end", "char": 18, "errors": [{"type": "parse-error"}]}, - { "src": "no {|literal} end", "char": 17, "errors": [{"type": "parse-error"}]}, - { "src": "no {|literal or placeholder end", "char": 31, "errors": [{"type": "parse-error"}]}, - { "src": ".local bar = {|foo|} {{_}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".local #bar = {|foo|} {{_}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".local $bar {|foo|} {{_}}", "char": 12, "errors": [{"type": "parse-error"}]}, - { "src": ".local $bar = |foo| {{_}}", "char": 14, "errors": [{"type": "parse-error"}]}, - { "src": ".match {#foo} * {{foo}}", "char": 8, "errors": [{"type": "parse-error"}]}, - { "src": ".match {} * {{foo}}", "char": 8, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}", "char": 30, "errors": [{"type": "parse-error"}]}, - { "src": ".match * {{foo}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|x| :x} * foo", "char": 21, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|x| :x} * {{foo}} extra", "char": 31, "errors": [{"type": "parse-error"}]}, - { "src": ".match |x| * {{foo}}", "char": 7, "errors": [{"type": "parse-error"}]}, - { "src": ".match {|foo| :string} o:ne {{one}} * {{other}}", "char": 24, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": ".match {|foo| :string} one: {{one}} * {{other}}", "char": 26, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": ".local $foo = {|42| :number option=a:b} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": ".local $foo = {|42| :number option=a:b:c} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" , "errors": [{"type": "parse-error"}]}, - { "src": "{$bar:foo}", "char": 5, "comment" : "tests for ':' in unquoted literals (not allowed)", "errors": [{"type": "parse-error"}]}, + "comment": "Unquoted literals can't begin with a ':'" + }, + { "src": "{|foo| {#markup}}", "char": 7, "comment": "Markup in wrong place" }, + { "src": "{|foo| #markup}", "char": 7, "comment": "Markup in wrong place" }, + { "src": "{|foo| {#markup/}}", "char": 7, "comment": "Markup in wrong place" }, + { "src": "{|foo| {/markup}}", "char": 7, "comment": "Markup in wrong place" }, + { "src": ".input $x = {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input $x = {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input {|1| :number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input {:number} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".input {|1|} {{{$x}}}", "char": 7, "comment": ".input with non-variable-expression" }, + { "src": ".", "char": 1}, + { "src": "{", "char": 1}, + { "src": "}", "char": 0}, + { "src": "{}", "char": 1}, + { "src": "{{", "char": 2}, + { "src": "{{}", "char": 3}, + { "src": "{{}}}", "char": 4}, + { "src": "{|foo| #markup}", "char": 7}, + { "src": "{{missing end brace}", "char": 20}, + { "src": "{{missing end braces", "char": 20}, + { "src": "{{missing end {$braces", "char": 22}, + { "src": "{{extra}} content", "char": 10}, + { "src": "empty { } placeholder", "char": 8}, + { "src": "missing space {42:func}", "char": 17}, + { "src": "missing space {|foo|:func}", "char": 20}, + { "src": "missing space {|foo|@bar}", "char": 20}, + { "src": "missing space {:func@bar}", "char": 20}, + { "src": "{:func @bar@baz}", "char": 11}, + { "src": "{:func @bar=42@baz}", "char": 14}, + { "src": "{+reserved@bar}", "char": 10}, + { "src": "{&private@bar}", "char": 9}, + { "src": "bad {:} placeholder", "char": 6}, + { "src": "bad {\\u0000placeholder}", "char": 5}, + { "src": "no-equal {|42| :number minimumFractionDigits 2}", "char": 45}, + { "src": "bad {:placeholder option=}", "char": 25}, + { "src": "bad {:placeholder option value}", "char": 25}, + { "src": "bad {:placeholder option:value}", "char": 30}, + { "src": "bad {:placeholder option}", "char": 24}, + { "src": "bad {:placeholder:}", "char": 18}, + { "src": "bad {::placeholder}", "char": 6}, + { "src": "bad {:placeholder::foo}", "char": 18}, + { "src": "bad {:placeholder option:=x}", "char": 25}, + { "src": "bad {:placeholder :option=x}", "char": 18}, + { "src": "bad {:placeholder option::x=y}", "char": 25}, + { "src": "bad {$placeholder option}", "char": 18}, + { "src": "bad {:placeholder @attribute=}", "char": 29}, + { "src": "bad {:placeholder @attribute=@foo}", "char": 29}, + { "src": "no {placeholder end", "char": 16}, + { "src": "no {$placeholder end", "char": 17}, + { "src": "no {:placeholder end", "char": 20}, + { "src": "no {|placeholder| end", "char": 18}, + { "src": "no {|literal} end", "char": 17}, + { "src": "no {|literal or placeholder end", "char": 31}, + { "src": ".local bar = {|foo|} {{_}}", "char": 7}, + { "src": ".local #bar = {|foo|} {{_}}", "char": 7}, + { "src": ".local $bar {|foo|} {{_}}", "char": 12}, + { "src": ".local $bar = |foo| {{_}}", "char": 14}, + { "src": ".match {#foo} * {{foo}}", "char": 8}, + { "src": ".match {} * {{foo}}", "char": 8}, + { "src": ".match {|foo| :x} {|bar| :x} ** {{foo}}", "char": 30}, + { "src": ".match * {{foo}}", "char": 7}, + { "src": ".match {|x| :x} * foo", "char": 21}, + { "src": ".match {|x| :x} * {{foo}} extra", "char": 31}, + { "src": ".match |x| * {{foo}}", "char": 7}, + { "src": ".match {|foo| :string} o:ne {{one}} * {{other}}", "char": 24, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": ".match {|foo| :string} one: {{one}} * {{other}}", "char": 26, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": ".local $foo = {|42| :number option=a:b} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": ".local $foo = {|42| :number option=a:b:c} {{bar {$foo}}}", "char": 36, "comment" : "tests for ':' in unquoted literals (not allowed)" }, + { "src": "{$bar:foo}", "char": 5, "comment" : "tests for ':' in unquoted literals (not allowed)"}, { "src": ".match {1} {{_}}", "char": 12, - "comment": "Disambiguating a wrong .match from an unsupported statement", - "errors": [{"type": "parse-error"}] + "comment": "Disambiguating a wrong .match from an unsupported statement" } -] + ] +} diff --git a/testdata/message2/syntax-errors-end-of-input.json b/testdata/message2/syntax-errors-end-of-input.json index 69fcc04d0cf3..d97dcb24ac82 100644 --- a/testdata/message2/syntax-errors-end-of-input.json +++ b/testdata/message2/syntax-errors-end-of-input.json @@ -1,4 +1,15 @@ -[ +{ + "scenario": "Syntax errors for unexpected end-of-input", + "description": "Syntax errors; for ICU4C, the character offset is expected to be the last character", + "defaultTestProperties": { + "locale": "en-US", + "expErrors": [ + { + "type": "syntax-error" + } + ] + }, + "tests": [ { "src": ".local ", "char": 9 }, { "src": ".lo", "char": 3 }, { "src": ".local $foo", "char": 11 }, @@ -13,4 +24,6 @@ { "src": "{{missing end {$brace}", "char": 22 }, { "src": "{{missing end {$brace}}", "char": 23 }, { "src": "{{%xyz", "char": 6 } -] + ] +} + diff --git a/testdata/message2/tricky-declarations.json b/testdata/message2/tricky-declarations.json index b340c46e90bf..3fded666e633 100644 --- a/testdata/message2/tricky-declarations.json +++ b/testdata/message2/tricky-declarations.json @@ -1,17 +1,19 @@ -[ +{ + "scenario": "Declarations tests", + "description": "Tests for interesting combinations of .local and .input", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": ".input {$var :number minimumFractionDigits=$var2} .input {$var2 :number minimumFractionDigits=5} {{{$var} {$var2}}}", "exp": "1.000 3.00000", - "params": { "var": 1, "var2": 3 }, - "errors": [{ "type": "duplicate-declaration" }] + "params": [{ "name": "var", "value": 1}, {"name": "var2", "value": 3 }], + "expErrors": [{ "type": "duplicate-declaration" }] }, { "src": ".local $var = {$var2} .local $var2 = {1} {{{$var} {$var2}}}", "exp": "5 1", - "params": { "var2": 5 }, - "errors": [{ "type": "duplicate-declaration" }] - }, - { - "src": ".local $var2 = {1} {{{$var2}}}", - "exp": "1", - "params": { "var2": 5 } + "params": [{ "name": "var2", "value": 5 }], + "expErrors": [{ "type": "duplicate-declaration" }] } -] + ] +} diff --git a/testdata/message2/valid-tests.json b/testdata/message2/valid-tests.json index f037c49d4b48..5f1292e00896 100644 --- a/testdata/message2/valid-tests.json +++ b/testdata/message2/valid-tests.json @@ -1,5 +1,10 @@ -[ - { "src": "hello {|4.2| :number}", "exp": "hello 4.2"}, +{ + "scenario": "Valid tests", + "description": "Additional valid tests", + "defaultTestProperties": { + "locale": "en-US" + }, + "tests": [ { "src": "hello {|4.2| :number minimumFractionDigits=2}", "exp": "hello 4.20"}, { "src": "hello {|4.2| :number minimumFractionDigits = 2}", "exp": "hello 4.20" }, { "src": "hello {|4.2| :number minimumFractionDigits= 2}", "exp": "hello 4.20" }, @@ -63,63 +68,61 @@ { "src": "{42e369}", "exp": "42e369", "ignoreJava": "See ICU-22810"}, { "src": "hello {|3| :number }", "exp": "hello 3" }, - { "src": "{:foo}", "errors": [{ "type": "missing-func" }], + { "src": "{:foo}", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k=v}", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k=v}", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k=v }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k=v }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k1=v1 k2=v2}", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k1=v1 k2=v2}", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{:foo k1=v1 k2=v2 }", "errors": [{ "type": "missing-func" }], + { "src": "{:foo k1=v1 k2=v2 }", "expErrors": [{ "type": "unknown-function" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, { "src": "{|3.14| }", "exp": "3.14" }, { "src": "{|3.14| }", "exp": "3.14" }, { "src": "{|3.14| :number}", "exp": "3.14" }, { "src": "{|3.14| :number }", "exp": "3.14" }, - { "src": "{$bar }", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar }", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar }", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar }", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar :foo}", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar :foo}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar :foo }", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar :foo }", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, - { "src": "{$bar-foo}", "errors": [{ "type": "unresolved-var" }], + { "src": "{$bar-foo}", "expErrors": [{ "type": "unresolved-variable" }], "ignoreJava": "See https://github.com/unicode-org/message-format-wg/issues/782" }, { "src": ".local $foo = {|hello|} .local $foo = {$foo} {{{$foo}}}", - "errors": [{ "type": "duplicate-declaration" }] }, + "expErrors": [{ "type": "duplicate-declaration" }] }, { "src": "good {placeholder}", "exp": "good placeholder" }, { "src": "a\\\\qbc", "exp": "a\\qbc", "comment": "pattern -> escaped-char -> backslash backslash" }, - { - "src": "{$one} and {$two}", - "params": { "one": "1.3", "two": "4.2" }, - "exp": "1.3 and 4.2" + "comment": "message -> simple-message -> simple-start pattern -> escaped-char", + "src": "\\\\", + "exp": "\\" }, { - "src": ".local $foo = {$baz} .local $bar = {$foo} {{bar {$bar}}}", - "params": { "baz": "foo" }, - "exp": "bar foo" + "comment": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"#\" identifier \"}\"", + "src": "{ #a}", + "exp": "" }, { - "src": ".local $foo = {$foo} {{bar {$foo}}}", - "params": { "foo": "foo" }, - "exp": "bar foo", - "errors": [{ "type": "duplicate-declaration" }] + "comment": "message -> simple-message -> simple-start pattern -> placeholder -> markup -> \"{\" s \"/\" identifier \"}\"", + "src": "{ /a}", + "exp": "" }, { - "src": ".local $foo = {$bar} .local $bar = {$baz} {{bar {$foo}}}", - "params": { "baz": "foo" }, - "exp": "bar {$bar}", - "errors": [{ "type": "duplicate-declaration" }] + "comment": "message -> complex-message -> *(declaration [s]) complex-body -> declaration complex-body -> input-declaration complex-body -> input variable-expression complex-body", + "src": ".input{$x}{{}}", + "exp": "" } -] + ] +}