commit 108d654abff40871cf52b549469502fc6a985e1d
parent 6c8aca084c9078fe3f1f260e3d4fdcbaefa1e9a4
Author: André Bargull <andre.bargull@gmail.com>
Date: Mon, 13 Oct 2025 12:55:24 +0000
Bug 1990248 - Part 2: Port CanonicalizeLocaleList to C++. r=spidermonkey-reviewers,jandem
The implementation code is in "intl/Locale.cpp" to reuse existing code, but at
some point this should be cleaned, so we no longer have different locale
identifier processing functions defined across multiple files (*).
The `HasAndGetElement` function was copied from "builtin/Array.cpp" and
slightly adjusted.
(*) CommonFunctions.cpp, IntlObject.cpp, LanguageTag.cpp, and Locale.cpp.
Differential Revision: https://phabricator.services.mozilla.com/D265824
Diffstat:
6 files changed, 195 insertions(+), 66 deletions(-)
diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp
@@ -311,6 +311,11 @@ static inline bool HasAndGetElement(JSContext* cx, HandleObject obj, T index,
return HasAndGetElement(cx, obj, obj, index, hole, vp);
}
+bool js::HasAndGetElement(JSContext* cx, HandleObject obj, uint64_t index,
+ bool* hole, MutableHandleValue vp) {
+ return HasAndGetElement(cx, obj, obj, index, hole, vp);
+}
+
bool ElementAdder::append(JSContext* cx, HandleValue v) {
MOZ_ASSERT(index_ < length_);
if (resObj_) {
@@ -1175,7 +1180,7 @@ JSString* js::ArrayToSource(JSContext* cx, HandleObject obj) {
for (uint64_t index = 0; index < length; index++) {
bool hole;
if (!CheckForInterrupt(cx) ||
- !HasAndGetElement(cx, obj, index, &hole, &elt)) {
+ !::HasAndGetElement(cx, obj, index, &hole, &elt)) {
return nullptr;
}
@@ -1687,8 +1692,8 @@ static bool array_reverse(JSContext* cx, unsigned argc, Value* vp) {
for (uint64_t i = 0, half = len / 2; i < half; i++) {
bool hole, hole2;
if (!CheckForInterrupt(cx) ||
- !HasAndGetElement(cx, obj, i, &hole, &lowval) ||
- !HasAndGetElement(cx, obj, len - i - 1, &hole2, &hival)) {
+ !::HasAndGetElement(cx, obj, i, &hole, &lowval) ||
+ !::HasAndGetElement(cx, obj, len - i - 1, &hole2, &hival)) {
return false;
}
@@ -2216,7 +2221,7 @@ static bool ArraySortWithoutComparator(JSContext* cx, Handle<JSObject*> obj,
}
bool hole;
- if (!HasAndGetElement(cx, obj, i, &hole, &v)) {
+ if (!::HasAndGetElement(cx, obj, i, &hole, &v)) {
return false;
}
if (hole) {
@@ -2393,7 +2398,7 @@ static MOZ_ALWAYS_INLINE bool ArraySortPrologue(JSContext* cx,
}
bool hole;
- if (!HasAndGetElement(cx, obj, i, &hole, &v)) {
+ if (!::HasAndGetElement(cx, obj, i, &hole, &v)) {
return false;
}
if (hole) {
@@ -2803,7 +2808,7 @@ static bool array_shift(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
bool hole;
- if (!HasAndGetElement(cx, obj, i + 1, &hole, &value)) {
+ if (!::HasAndGetElement(cx, obj, i + 1, &hole, &value)) {
return false;
}
if (hole) {
@@ -2906,7 +2911,7 @@ static bool array_unshift(JSContext* cx, unsigned argc, Value* vp) {
return false;
}
bool hole;
- if (!HasAndGetElement(cx, obj, last, &hole, &value)) {
+ if (!::HasAndGetElement(cx, obj, last, &hole, &value)) {
return false;
}
if (hole) {
@@ -3045,7 +3050,7 @@ static bool CopyArrayElements(JSContext* cx, HandleObject obj, uint64_t begin,
for (; index < limit; index++) {
bool hole;
if (!CheckForInterrupt(cx) ||
- !HasAndGetElement(cx, obj, begin + index, &hole, &value)) {
+ !::HasAndGetElement(cx, obj, begin + index, &hole, &value)) {
return false;
}
@@ -3073,7 +3078,7 @@ static bool CopyArrayElements(JSContext* cx, HandleObject obj, uint64_t begin,
for (uint64_t i = startIndex; i < count; i++) {
bool hole;
if (!CheckForInterrupt(cx) ||
- !HasAndGetElement(cx, obj, begin + i, &hole, &value)) {
+ !::HasAndGetElement(cx, obj, begin + i, &hole, &value)) {
return false;
}
@@ -3228,7 +3233,7 @@ static bool array_splice_impl(JSContext* cx, unsigned argc, Value* vp,
/* Steps 13.b, 13.c.i. */
bool hole;
- if (!HasAndGetElement(cx, obj, actualStart + k, &hole, &fromValue)) {
+ if (!::HasAndGetElement(cx, obj, actualStart + k, &hole, &fromValue)) {
return false;
}
@@ -3290,7 +3295,7 @@ static bool array_splice_impl(JSContext* cx, unsigned argc, Value* vp,
/* Steps 16.b.iii-v */
bool hole;
- if (!HasAndGetElement(cx, obj, from, &hole, &fromValue)) {
+ if (!::HasAndGetElement(cx, obj, from, &hole, &fromValue)) {
return false;
}
@@ -3389,7 +3394,7 @@ static bool array_splice_impl(JSContext* cx, unsigned argc, Value* vp,
/* Steps 17.b.iii, 17.b.iv.1. */
bool hole;
- if (!HasAndGetElement(cx, obj, from, &hole, &fromValue)) {
+ if (!::HasAndGetElement(cx, obj, from, &hole, &fromValue)) {
return false;
}
@@ -3988,7 +3993,7 @@ static bool SliceSparse(JSContext* cx, HandleObject obj, uint64_t begin,
MOZ_ASSERT(begin <= index && index < end);
bool hole;
- if (!HasAndGetElement(cx, obj, index, &hole, &value)) {
+ if (!::HasAndGetElement(cx, obj, index, &hole, &value)) {
return false;
}
@@ -4152,7 +4157,7 @@ static bool array_slice(JSContext* cx, unsigned argc, Value* vp) {
/* Steps 10.a-b, and 10.c.i. */
bool kNotPresent;
- if (!HasAndGetElement(cx, obj, k, &kNotPresent, &kValue)) {
+ if (!::HasAndGetElement(cx, obj, k, &kNotPresent, &kValue)) {
return false;
}
@@ -4554,7 +4559,7 @@ bool js::array_indexOf(JSContext* cx, unsigned argc, Value* vp) {
}
bool hole;
- if (!HasAndGetElement(cx, obj, k, &hole, &v)) {
+ if (!::HasAndGetElement(cx, obj, k, &hole, &v)) {
return false;
}
if (hole) {
@@ -4668,7 +4673,7 @@ bool js::array_lastIndexOf(JSContext* cx, unsigned argc, Value* vp) {
}
bool hole;
- if (!HasAndGetElement(cx, obj, uint64_t(i), &hole, &v)) {
+ if (!::HasAndGetElement(cx, obj, uint64_t(i), &hole, &v)) {
return false;
}
if (hole) {
@@ -5056,7 +5061,7 @@ static bool array_concat(JSContext* cx, unsigned argc, Value* vp) {
// Step 5.b.iv.2.
bool hole;
- if (!HasAndGetElement(cx, obj, k, &hole, &v)) {
+ if (!::HasAndGetElement(cx, obj, k, &hole, &v)) {
return false;
}
if (!hole) {
diff --git a/js/src/builtin/Array.h b/js/src/builtin/Array.h
@@ -113,6 +113,13 @@ extern bool SetLengthProperty(JSContext* cx, HandleObject obj, uint32_t length);
extern bool GetElements(JSContext* cx, HandleObject aobj, uint32_t length,
js::Value* vp);
+/*
+ * If the property at the given index exists, get its value into |vp| and set
+ * |*hole| to false. Otherwise set |*hole| to true and |vp| to Undefined.
+ */
+extern bool HasAndGetElement(JSContext* cx, HandleObject obj, uint64_t index,
+ bool* hole, MutableHandleValue vp);
+
/* Natives exposed for optimization by the interpreter and JITs. */
extern bool array_includes(JSContext* cx, unsigned argc, js::Value* vp);
diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h
@@ -10,6 +10,7 @@
#include <stddef.h>
#include <stdint.h>
+#include "js/GCVector.h"
#include "js/RootingAPI.h"
#include "js/Utility.h"
@@ -95,6 +96,11 @@ extern const OldStyleLanguageTagMapping oldStyleLanguageTagMappings[5];
extern JS::UniqueChars EncodeLocale(JSContext* cx, JSString* locale);
+using LocalesList = JS::StackGCVector<JSLinearString*>;
+
+bool CanonicalizeLocaleList(JSContext* cx, JS::Handle<JS::Value> locales,
+ JS::MutableHandle<LocalesList> result);
+
// The inline capacity we use for a Vector<char16_t>. Use this to ensure that
// our uses of ICU string functions, below and elsewhere, will try to fill the
// buffer's entire inline capacity before growing it and heap-allocating.
diff --git a/js/src/builtin/intl/LanguageTag.cpp b/js/src/builtin/intl/LanguageTag.cpp
@@ -191,7 +191,7 @@ static bool IsAsciiAlpha(const JSLinearString* str) {
return IsAsciiAlpha<char16_t>(str->twoByteRange(nogc));
}
-JS::Result<JSString*> js::intl::ParseStandaloneISO639LanguageTag(
+JS::Result<JSLinearString*> js::intl::ParseStandaloneISO639LanguageTag(
JSContext* cx, Handle<JSLinearString*> str) {
// ISO-639 language codes contain either two or three characters.
size_t length = str->length();
@@ -229,7 +229,7 @@ JS::Result<JSString*> js::intl::ParseStandaloneISO639LanguageTag(
}
// Take care to replace deprecated subtags with their preferred values.
- JSString* result;
+ JSLinearString* result;
if (mozilla::intl::Locale::LanguageMapping(languageTag) || !isLowerCase) {
result = NewStringCopy<CanGC>(cx, languageTag.Span());
} else {
diff --git a/js/src/builtin/intl/LanguageTag.h b/js/src/builtin/intl/LanguageTag.h
@@ -18,7 +18,6 @@
struct JS_PUBLIC_API JSContext;
class JSLinearString;
-class JS_PUBLIC_API JSString;
class JS_PUBLIC_API JSTracer;
namespace js {
@@ -67,7 +66,7 @@ namespace intl {
* the input could not be parsed or the canonical form of the resulting language
* tag contains more than a single language subtag.
*/
-JS::Result<JSString*> ParseStandaloneISO639LanguageTag(
+JS::Result<JSLinearString*> ParseStandaloneISO639LanguageTag(
JSContext* cx, JS::Handle<JSLinearString*> str);
class UnicodeExtensionKeyword final {
diff --git a/js/src/builtin/intl/Locale.cpp b/js/src/builtin/intl/Locale.cpp
@@ -20,6 +20,7 @@
#include <string.h>
#include <utility>
+#include "builtin/Array.h"
#include "builtin/Boolean.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/FormatBuffer.h"
@@ -1396,56 +1397,23 @@ const ClassSpec LocaleObject::classSpec_ = {
ClassSpec::DontDefineConstructor,
};
-bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc,
- Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
- MOZ_ASSERT(args.length() == 2);
-
- HandleValue tagValue = args[0];
- bool applyToString = args[1].toBoolean();
-
- if (tagValue.isObject()) {
- JSString* tagStr;
- JS_TRY_VAR_OR_RETURN_FALSE(
- cx, tagStr,
- LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
- if (tagStr) {
- args.rval().setString(tagStr);
- return true;
- }
- }
-
- if (!applyToString && !tagValue.isString()) {
- args.rval().setNull();
- return true;
- }
-
- JSString* tagStr = ToString(cx, tagValue);
- if (!tagStr) {
- return false;
- }
-
- Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx));
- if (!tagLinearStr) {
- return false;
- }
-
+static JSLinearString* ValidateAndCanonicalizeLanguageTag(
+ JSContext* cx, Handle<JSLinearString*> string) {
// Handle the common case (a standalone language) first.
// Only the following Unicode BCP 47 locale identifier subset is accepted:
// unicode_locale_id = unicode_language_id
// unicode_language_id = unicode_language_subtag
// unicode_language_subtag = alpha{2,3}
- JSString* language;
- JS_TRY_VAR_OR_RETURN_FALSE(
- cx, language, intl::ParseStandaloneISO639LanguageTag(cx, tagLinearStr));
+ JSLinearString* language;
+ JS_TRY_VAR_OR_RETURN_NULL(cx, language,
+ intl::ParseStandaloneISO639LanguageTag(cx, string));
if (language) {
- args.rval().setString(language);
- return true;
+ return language;
}
mozilla::intl::Locale tag;
- if (!intl::ParseLocale(cx, tagLinearStr, tag)) {
- return false;
+ if (!intl::ParseLocale(cx, string, tag)) {
+ return nullptr;
}
auto result = tag.Canonicalize();
@@ -1457,20 +1425,70 @@ bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc,
} else {
intl::ReportInternalError(cx);
}
- return false;
+ return nullptr;
}
intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx);
if (auto result = tag.ToString(buffer); result.isErr()) {
intl::ReportInternalError(cx, result.unwrapErr());
- return false;
+ return nullptr;
}
- JSString* resultStr = buffer.toAsciiString(cx);
+ return buffer.toAsciiString(cx);
+}
+
+static JSLinearString* ValidateAndCanonicalizeLanguageTag(
+ JSContext* cx, Handle<Value> tagValue) {
+ if (tagValue.isObject()) {
+ JSString* tagStr;
+ JS_TRY_VAR_OR_RETURN_NULL(
+ cx, tagStr,
+ LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
+ if (tagStr) {
+ return tagStr->ensureLinear(cx);
+ }
+ }
+
+ JSString* tagStr = ToString(cx, tagValue);
+ if (!tagStr) {
+ return nullptr;
+ }
+
+ Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx));
+ if (!tagLinearStr) {
+ return nullptr;
+ }
+ return ValidateAndCanonicalizeLanguageTag(cx, tagLinearStr);
+}
+
+bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ MOZ_ASSERT(args.length() == 2);
+
+ HandleValue tagValue = args[0];
+ bool applyToString = args[1].toBoolean();
+
+ if (tagValue.isObject()) {
+ JSString* tagStr;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, tagStr,
+ LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject()));
+ if (tagStr) {
+ args.rval().setString(tagStr);
+ return true;
+ }
+ }
+
+ if (!applyToString && !tagValue.isString()) {
+ args.rval().setNull();
+ return true;
+ }
+
+ auto* resultStr = ValidateAndCanonicalizeLanguageTag(cx, tagValue);
if (!resultStr) {
return false;
}
-
args.rval().setString(resultStr);
return true;
}
@@ -1617,3 +1635,97 @@ bool js::intl_ValidateAndCanonicalizeUnicodeExtensionType(JSContext* cx,
args.rval().setString(result);
return true;
}
+
+/**
+ * Canonicalizes a locale list.
+ *
+ * Spec: ECMAScript Internationalization API Specification, 9.2.1.
+ */
+bool js::intl::CanonicalizeLocaleList(JSContext* cx, Handle<Value> locales,
+ MutableHandle<LocalesList> result) {
+ MOZ_ASSERT(result.empty());
+
+ // Step 1.
+ if (locales.isUndefined()) {
+ return true;
+ }
+
+ // Step 3 (and the remaining steps).
+ if (locales.isString()) {
+ Rooted<JSLinearString*> linear(cx, locales.toString()->ensureLinear(cx));
+ if (!linear) {
+ return false;
+ }
+
+ auto* languageTag = ValidateAndCanonicalizeLanguageTag(cx, linear);
+ if (!languageTag) {
+ return false;
+ }
+ return result.append(languageTag);
+ }
+
+ if (locales.isObject()) {
+ JSString* languageTag;
+ JS_TRY_VAR_OR_RETURN_FALSE(
+ cx, languageTag,
+ LanguageTagFromMaybeWrappedLocale(cx, &locales.toObject()));
+ if (languageTag) {
+ auto* linear = languageTag->ensureLinear(cx);
+ if (!linear) {
+ return false;
+ }
+ return result.append(linear);
+ }
+ }
+
+ // Step 2. (Implicit)
+
+ // Step 4.
+ Rooted<JSObject*> obj(cx, ToObject(cx, locales));
+ if (!obj) {
+ return false;
+ }
+
+ // Step 5.
+ uint64_t length;
+ if (!GetLengthProperty(cx, obj, &length)) {
+ return false;
+ }
+
+ // Steps 6-7.
+ Rooted<Value> value(cx);
+ for (uint64_t k = 0; k < length; k++) {
+ // Step 7.a-c.
+ bool hole;
+ if (!CheckForInterrupt(cx) ||
+ !HasAndGetElement(cx, obj, k, &hole, &value)) {
+ return false;
+ }
+
+ if (!hole) {
+ // Step 7.c.ii.
+ if (!value.isString() && !value.isObject()) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_INVALID_LOCALES_ELEMENT);
+ return false;
+ }
+
+ // Step 7.c.iii-iv.
+ JSLinearString* tag = ValidateAndCanonicalizeLanguageTag(cx, value);
+ if (!tag) {
+ return false;
+ }
+
+ // Step 7.c.v.
+ bool addToResult =
+ std::none_of(result.begin(), result.end(),
+ [tag](auto* other) { return EqualStrings(tag, other); });
+ if (addToResult && !result.append(tag)) {
+ return false;
+ }
+ }
+ }
+
+ // Step 8.
+ return true;
+}