Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr
throw new XPathException(this, ErrorCodes.XPTY0004, "Cannot cast " + Type.getTypeName(item.getType()) + " to xs:QName");
}
} else {
// Per XPath F&O 3.1, Section 19: if the source and target types
// have no valid casting relationship, raise XPTY0004 (not FORG0001).
if (!Type.isCastable(item.getType(), requiredType)) {
throw new XPathException(this, ErrorCodes.XPTY0004,
"Cannot cast " + Type.getTypeName(item.getType()) +
" to " + Type.getTypeName(requiredType));
}
result = item.convertTo(requiredType);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,19 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
}
else {
try {
// Per XPath F&O 3.1: if the cast is impossible per the casting table,
// castable as returns false without attempting the conversion.
if (!Type.isCastable(seq.itemAt(0).getType(), requiredType)) {
result = BooleanValue.FALSE;
} else {
seq.itemAt(0).convertTo(requiredType);
//If ? is specified after the target type, the result of the cast expression is an empty sequence.
if (requiredCardinality.isSuperCardinalityOrEqualOf(seq.getCardinality()))
{result = BooleanValue.TRUE;}
//If ? is not specified after the target type, a type error is raised [err:XPTY0004].
else
{result = BooleanValue.FALSE;}
}
//TODO : improve by *not* using a costly exception ?
} catch(final XPathException e) {
result = BooleanValue.FALSE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
{max = value;}

else {
if (Type.getCommonSuperType(max.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE) {
if (Type.getCommonSuperType(max.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE
&& !(Type.subTypeOfUnion(max.getType(), Type.NUMERIC)
&& Type.subTypeOfUnion(value.getType(), Type.NUMERIC))) {
throw new XPathException(this, ErrorCodes.FORG0006, "Cannot compare " + Type.getTypeName(max.getType()) +
" and " + Type.getTypeName(value.getType()), max);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
if (min == null)
{min = value;}
else {
if (Type.getCommonSuperType(min.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE) {
if (Type.getCommonSuperType(min.getType(), value.getType()) == Type.ANY_ATOMIC_TYPE
&& !(Type.subTypeOfUnion(min.getType(), Type.NUMERIC)
&& Type.subTypeOfUnion(value.getType(), Type.NUMERIC))) {
throw new XPathException(this, ErrorCodes.FORG0006, "Cannot compare " + Type.getTypeName(min.getType()) +
" and " + Type.getTypeName(value.getType()), value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ public AtomicValue convertTo(final int requiredType) throws XPathException {
case Type.UNTYPED_ATOMIC:
return new UntypedAtomicValue(getExpression(), getStringValue());
default:
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
// Handle integer subtypes (nonPositiveInteger, negativeInteger, etc.)
if (Type.subTypeOf(requiredType, Type.INTEGER)) {
return new IntegerValue(getExpression(), value ? 1 : 0).convertTo(requiredType);
}
throw new XPathException(getExpression(), ErrorCodes.FORG0001,
"cannot convert 'xs:boolean(" + value + ")' to " + Type.getTypeName(requiredType));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,21 +195,27 @@ public AtomicValue convertTo(final int requiredType) throws XPathException {

public DecimalValue toDecimalValue() throws XPathException {
if (isNaN() || isInfinite()) {
throw conversionError(Type.DECIMAL);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no longer using the existing conversionError()method?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was the method call conversionError() removed and code duplication introduced? we could pass in an additional error code for the other usages instead in order to reduce code duplication.

throw new XPathException(getExpression(), ErrorCodes.FOCA0002,
"Cannot convert " + Type.getTypeName(getType()) + "('" + getStringValue()
+ "') to " + Type.getTypeName(Type.DECIMAL));
}
return new DecimalValue(getExpression(), BigDecimal.valueOf(value));
}

public IntegerValue toIntegerValue() throws XPathException {
if (isNaN() || isInfinite()) {
throw conversionError(Type.INTEGER);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no longer using the existing conversionError()method?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in toDecimalValue()

throw new XPathException(getExpression(), ErrorCodes.FOCA0002,
"Cannot convert " + Type.getTypeName(getType()) + "('" + getStringValue()
+ "') to " + Type.getTypeName(Type.INTEGER));
}
return new IntegerValue(getExpression(), (long) value);
}

public IntegerValue toIntegerSubType(final int subType) throws XPathException {
if (isNaN() || isInfinite()) {
throw conversionError(subType);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no longer using the existing conversionError()method?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in toDecimalValue()

throw new XPathException(getExpression(), ErrorCodes.FOCA0002,
"Cannot convert " + Type.getTypeName(getType()) + "('" + getStringValue()
+ "') to " + Type.getTypeName(subType));
}
if (subType != Type.INTEGER && value > Integer.MAX_VALUE) {
throw new XPathException(getExpression(), ErrorCodes.FOCA0003, "Value is out of range for type "
Expand Down
111 changes: 111 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/value/Type.java
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,117 @@
return primitiveType;
}

/**
* Check if a cast from sourceType to targetType is allowed per XPath F&O 3.1
* Section 19 (Casting), Table 6. This implements the casting table that determines
* whether a cast between two primitive types is possible (may succeed for some values)
* or impossible (will never succeed for any value).
*
* <p>If the cast is impossible, the caller should raise XPTY0004 rather than
* attempting the cast (which would incorrectly raise FORG0001).</p>
*
* @param sourceType the type constant of the source type
* @param targetType the type constant of the target type
*
* @return true if the cast may succeed (for some values), false if the cast can never succeed
*/
public static boolean isCastable(final int sourceType, final int targetType) {

Check warning on line 733 in exist-core/src/main/java/org/exist/xquery/value/Type.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/xquery/value/Type.java#L733

The method 'isCastable(int, int)' has an NPath complexity of 8960, current threshold is 200
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the the NPath complexity of 8960, current threshold is 200.

// Casting to/from ITEM, ANY_ATOMIC_TYPE, or same type is always allowed
if (sourceType == targetType || targetType == ITEM || targetType == ANY_ATOMIC_TYPE) {
return true;
}

// xs:untypedAtomic and xs:string can be cast to any atomic type
if (sourceType == UNTYPED_ATOMIC || sourceType == STRING || subTypeOf(sourceType, STRING)) {
return true;
}

// Any atomic type can be cast to xs:untypedAtomic or xs:string
if (targetType == UNTYPED_ATOMIC || targetType == STRING || subTypeOf(targetType, STRING)) {
return true;
}

// Get primitive types for the casting table lookup
final int srcPrimitive;
final int tgtPrimitive;
try {
srcPrimitive = primitiveTypeOf(sourceType);
tgtPrimitive = primitiveTypeOf(targetType);
} catch (final IllegalArgumentException e) {
// Unknown type — allow the cast to proceed and let convertTo() handle errors
return true;
}

// Same primitive type is always castable
if (srcPrimitive == tgtPrimitive) {
return true;
}

// XPath F&O 3.1, Section 19, Table 6 — Casting table by primitive type pairs.
// 'M' (may) or 'Y' (yes) entries return true; 'N' (no) entries return false.
return switch (srcPrimitive) {
case FLOAT, DOUBLE, DECIMAL ->
// Numerics can cast to: other numerics, boolean, duration subtypes
tgtPrimitive == FLOAT || tgtPrimitive == DOUBLE || tgtPrimitive == DECIMAL
|| tgtPrimitive == BOOLEAN;

case BOOLEAN ->
// Boolean can cast to: numerics
tgtPrimitive == FLOAT || tgtPrimitive == DOUBLE || tgtPrimitive == DECIMAL;

case DURATION ->
// Duration can cast to: duration subtypes (yearMonthDuration, dayTimeDuration)
// duration subtypes share the same primitive: DURATION
// So this is already handled by srcPrimitive == tgtPrimitive above
false;

case DATE_TIME ->
// dateTime can cast to: date, time, gYearMonth, gYear, gMonthDay, gDay, gMonth
tgtPrimitive == DATE || tgtPrimitive == TIME
|| tgtPrimitive == G_YEAR_MONTH || tgtPrimitive == G_YEAR
|| tgtPrimitive == G_MONTH_DAY || tgtPrimitive == G_DAY
|| tgtPrimitive == G_MONTH;

case DATE ->
// date can cast to: dateTime, gYearMonth, gYear, gMonthDay, gDay, gMonth
tgtPrimitive == DATE_TIME
|| tgtPrimitive == G_YEAR_MONTH || tgtPrimitive == G_YEAR
|| tgtPrimitive == G_MONTH_DAY || tgtPrimitive == G_DAY
|| tgtPrimitive == G_MONTH;

case TIME ->
// time CANNOT cast to anything except string/untypedAtomic (handled above)
false;

case G_YEAR_MONTH, G_YEAR, G_MONTH_DAY, G_DAY, G_MONTH ->
// Gregorian types can only cast to: dateTime (if enough info), but spec says N
// except string/untypedAtomic (handled above)
false;

case HEX_BINARY ->
// hexBinary can cast to: base64Binary
tgtPrimitive == BASE64_BINARY;

case BASE64_BINARY ->
// base64Binary can cast to: hexBinary
tgtPrimitive == HEX_BINARY;

case ANY_URI ->
// anyURI cannot cast to anything except string/untypedAtomic (handled above)
false;

case QNAME ->
// QName cannot cast to anything except string/untypedAtomic (handled above)
// (and NOTATION, but that's rarely used)
tgtPrimitive == NOTATION;

case NOTATION ->
tgtPrimitive == QNAME;

default -> true; // Unknown — allow and let convertTo() decide
};
}

/**
* Get the XDM equivalent type of a DOM Node type (i.e. {@link Node#getNodeType()}).
*
Expand Down
Loading