Allow undefined variants in IDL unions (#37042)

Testing: Includes a test which ensures that unions with a `undefined`
variant compile
Fixes https://github.com/servo/servo/issues/28679
Part of https://github.com/servo/servo/issues/30287

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-05-23 18:16:22 +02:00 committed by GitHub
parent 9c0c1cf30f
commit fcc391d852
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 10 deletions

View file

@ -39,6 +39,7 @@ from WebIDL import (
from Configuration import ( from Configuration import (
Configuration, Configuration,
Descriptor, Descriptor,
DescriptorProvider,
MakeNativeName, MakeNativeName,
MemberIsLegacyUnforgeable, MemberIsLegacyUnforgeable,
getModuleFromObject, getModuleFromObject,
@ -617,16 +618,16 @@ class JSToNativeConversionInfo():
def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None, def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None,
isDefinitelyObject=False, isDefinitelyObject: bool = False,
isMember=False, isMember: bool | str = False,
isArgument=False, isArgument=False,
isAutoRooted=False, isAutoRooted=False,
invalidEnumValueFatal=True, invalidEnumValueFatal=True,
defaultValue=None, defaultValue=None,
exceptionCode=None, exceptionCode=None,
allowTreatNonObjectAsNull=False, allowTreatNonObjectAsNull: bool = False,
isCallbackReturnValue=False, isCallbackReturnValue=False,
sourceDescription="value"): sourceDescription="value") -> JSToNativeConversionInfo:
""" """
Get a template for converting a JS value to a native object based on the Get a template for converting a JS value to a native object based on the
given type and descriptor. If failureCode is given, then we're actually given type and descriptor. If failureCode is given, then we're actually
@ -5071,7 +5072,7 @@ class CGConstant(CGThing):
return f"pub const {name}: {const_type} = {value};\n" return f"pub const {name}: {const_type} = {value};\n"
def getUnionTypeTemplateVars(type, descriptorProvider): def getUnionTypeTemplateVars(type, descriptorProvider: DescriptorProvider):
if type.isGeckoInterface(): if type.isGeckoInterface():
name = type.inner.identifier.name name = type.inner.identifier.name
typeName = descriptorProvider.getDescriptor(name).returnType typeName = descriptorProvider.getDescriptor(name).returnType
@ -5108,6 +5109,12 @@ def getUnionTypeTemplateVars(type, descriptorProvider):
elif type.isCallback(): elif type.isCallback():
name = type.name name = type.name
typeName = f"{name}<D>" typeName = f"{name}<D>"
elif type.isUndefined():
return {
"name": type.name,
"typeName": "()",
"jsConversion": CGGeneric("if value.is_undefined() { Ok(Some(())) } else { Ok(None) }")
}
else: else:
raise TypeError(f"Can't handle {type} in unions yet") raise TypeError(f"Can't handle {type} in unions yet")
@ -5173,7 +5180,7 @@ impl{self.generic} Clone for {self.type}{self.genericSuffix} {{
def manualImpl(self, t, templateVars): def manualImpl(self, t, templateVars):
if t == "Clone": if t == "Clone":
return self.manualImplClone(templateVars) return self.manualImplClone(templateVars)
raise f"Don't know how to impl {t} for union" raise ValueError(f"Don't know how to impl {t} for union")
def define(self): def define(self):
def getTypeWrapper(t): def getTypeWrapper(t):
@ -5330,31 +5337,44 @@ class CGUnionConversionStruct(CGThing):
stringTypes = [t for t in memberTypes if t.isString() or t.isEnum()] stringTypes = [t for t in memberTypes if t.isString() or t.isEnum()]
numericTypes = [t for t in memberTypes if t.isNumeric()] numericTypes = [t for t in memberTypes if t.isNumeric()]
booleanTypes = [t for t in memberTypes if t.isBoolean()] booleanTypes = [t for t in memberTypes if t.isBoolean()]
if stringTypes or numericTypes or booleanTypes: numUndefinedVariants = [t.isUndefined() for t in memberTypes].count(True)
if stringTypes or numericTypes or booleanTypes or numUndefinedVariants != 0:
assert len(stringTypes) <= 1 assert len(stringTypes) <= 1
assert len(numericTypes) <= 1 assert len(numericTypes) <= 1
assert len(booleanTypes) <= 1 assert len(booleanTypes) <= 1
assert numUndefinedVariants <= 1
def getStringOrPrimitiveConversion(memberType): def getStringOrPrimitiveConversion(memberType):
typename = get_name(memberType) typename = get_name(memberType)
return CGGeneric(get_match(typename)) return CGGeneric(get_match(typename))
other = [] other = []
stringConversion = list(map(getStringOrPrimitiveConversion, stringTypes)) stringConversion = list(map(getStringOrPrimitiveConversion, stringTypes))
numericConversion = list(map(getStringOrPrimitiveConversion, numericTypes)) numericConversion = list(map(getStringOrPrimitiveConversion, numericTypes))
booleanConversion = list(map(getStringOrPrimitiveConversion, booleanTypes)) booleanConversion = list(map(getStringOrPrimitiveConversion, booleanTypes))
undefinedConversion = CGGeneric("return Ok(ConversionResult::Success(Self::Undefined(())));")
if stringConversion: if stringConversion:
if booleanConversion: if booleanConversion:
other.append(CGIfWrapper("value.get().is_boolean()", booleanConversion[0])) other.append(CGIfWrapper("value.get().is_boolean()", booleanConversion[0]))
if numericConversion: if numericConversion:
other.append(CGIfWrapper("value.get().is_number()", numericConversion[0])) other.append(CGIfWrapper("value.get().is_number()", numericConversion[0]))
if numUndefinedVariants != 0:
other.append(CGIfWrapper("value.get().is_undefined()", undefinedConversion))
other.append(stringConversion[0]) other.append(stringConversion[0])
elif numericConversion: elif numericConversion:
if booleanConversion: if booleanConversion:
other.append(CGIfWrapper("value.get().is_boolean()", booleanConversion[0])) other.append(CGIfWrapper("value.get().is_boolean()", booleanConversion[0]))
if numUndefinedVariants != 0:
other.append(CGIfWrapper("value.get().is_undefined()", undefinedConversion))
other.append(numericConversion[0]) other.append(numericConversion[0])
else: elif booleanConversion:
assert booleanConversion if numUndefinedVariants != 0:
other.append(CGIfWrapper("value.get().is_undefined()", undefinedConversion))
other.append(booleanConversion[0]) other.append(booleanConversion[0])
else:
assert numUndefinedVariants != 0
other.append(undefinedConversion)
conversions.append(CGList(other, "\n\n")) conversions.append(CGList(other, "\n\n"))
conversions.append(CGGeneric( conversions.append(CGGeneric(
f'Ok(ConversionResult::Failure("argument could not be converted to any of: {", ".join(names)}".into()))' f'Ok(ConversionResult::Failure("argument could not be converted to any of: {", ".join(names)}".into()))'
@ -5376,6 +5396,10 @@ class CGUnionConversionStruct(CGThing):
post="\n}") post="\n}")
def try_method(self, t): def try_method(self, t):
if t.isUndefined():
# undefined does not require a conversion method, so we don't generate one
return CGGeneric("")
templateVars = getUnionTypeTemplateVars(t, self.descriptorProvider) templateVars = getUnionTypeTemplateVars(t, self.descriptorProvider)
actualType = templateVars["typeName"] actualType = templateVars["typeName"]
if type_needs_tracing(t): if type_needs_tracing(t):
@ -7151,7 +7175,7 @@ impl{self.generic} Clone for {self.makeClassName(self.dictionary)}{self.genericS
def manualImpl(self, t): def manualImpl(self, t):
if t == "Clone": if t == "Clone":
return self.manualImplClone() return self.manualImplClone()
raise f"Don't know how to impl {t} for dicts." raise ValueError(f"Don't know how to impl {t} for dicts.")
def struct(self): def struct(self):
d = self.dictionary d = self.dictionary

View file

@ -627,3 +627,6 @@ dictionary NotUsedAnyWhereElse {};
dictionary RecordFieldWithUnionInside { dictionary RecordFieldWithUnionInside {
record<USVString, (USVString or NotUsedAnyWhereElse)> recordWithUnionField; record<USVString, (USVString or NotUsedAnyWhereElse)> recordWithUnionField;
}; };
// https://github.com/servo/servo/issues/28679
typedef (USVString or undefined) UnionWithUndefinedVariant;