Skip to content

Conversation

@dondonz
Copy link
Member

@dondonz dondonz commented Oct 10, 2022

The GraphQL Float spec was updated a year ago to specifically not allow NaN and positive/negative infinity in both result coercion and input coercion

https://spec.graphql.org/draft/#sec-Float
graphql/graphql-spec#778

The float coercing was mostly compliant with this spec update, as NaN and positive/negative infinity are not allowed in the BigDecimal String constructor used in GraphqlFloatCoercing, specifically the line value = new BigDecimal(input.toString());.

I say mostly, there was one uncaught case. It's possible for the old convertImpl method to return a Double value that is later converted to positive infinity with BigDecimal.valueOf, such as in the valueToLiteralImpl

private FloatValue valueToLiteralImpl(Object input, @NotNull Locale locale) {
    Double result = assertNotNull(convertImpl(input), () -> i18nMsg(locale, "Float.notFloat", typeName(input)));
    return FloatValue.newFloatValue(BigDecimal.valueOf(result)).build();
}

This PR introduces an explicit check for positive/negative infinity and NaN to address this problem. This PR cleans up NaN and infinity tests.

return null;
}
return doubleInput;
} else if (isNumberIsh(input)) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I did not add a separate else if (input instanceof Float) branch because it causes some tests to fail.

Getting the doubleValue() from a BigDecimal produces an ever-so-slightly-different result to getting the doubleValue() out of a Float.

Copy link
Member

Choose a reason for hiding this comment

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

Current this is

private Double convertImpl(Object input) {
        // From the GraphQL Float spec, non-finite floating-point internal values (NaN and Infinity)
        // must raise a field error on both result and input coercion
        if (input instanceof Double) {
            Double doubleInput = (Double) input;
            if (Double.isNaN(doubleInput) || Double.isInfinite(doubleInput)) {
                return null;
            }
            return doubleInput;
        } else if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                return null;
            }
            return value.doubleValue();
        } else {
            return null;
        }
    }

So it does not apply the checks to the BigDecimal derived string

Why not do

private Double convertImpl(Object input) {
        // From the GraphQL Float spec, non-finite floating-point internal values (NaN and Infinity)
        // must raise a field error on both result and input coercion
        Double doubleInput;
        if (input instanceof Double) {
            doubleInput = (Double) input;
        } else if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                return null;
            }
            doubleInput =  value.doubleValue();
        } else {
            return null;
        }
        
        if (Double.isNaN(doubleInput) || Double.isInfinite(doubleInput)) {
                return null;
        }
       return doubleInput
    }

Forgive me if this doesnt compile

Copy link
Member

Choose a reason for hiding this comment

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

This seems more sensible - eg first get it into a double and then check it - regardless

Even if BigDecimal never produces Nan or infinity, the logic reads better

Copy link
Member Author

@dondonz dondonz Oct 27, 2022

Choose a reason for hiding this comment

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

Oh no, it can produce infinity! This was a great idea


public static final GraphQLObjectType queryType = newObject()
.name("QueryType")
// Static scalars
Copy link
Member Author

Choose a reason for hiding this comment

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

This was added a long time ago, before the GraphQL Float spec was updated to specifically not allow NaN and positive/negative infinity values

@dondonz dondonz added this to the 20.0 milestone Oct 10, 2022
Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

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

Can we tweak it as suggested or justify why not :)

return null;
}
return doubleInput;
} else if (isNumberIsh(input)) {
Copy link
Member

Choose a reason for hiding this comment

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

Current this is

private Double convertImpl(Object input) {
        // From the GraphQL Float spec, non-finite floating-point internal values (NaN and Infinity)
        // must raise a field error on both result and input coercion
        if (input instanceof Double) {
            Double doubleInput = (Double) input;
            if (Double.isNaN(doubleInput) || Double.isInfinite(doubleInput)) {
                return null;
            }
            return doubleInput;
        } else if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                return null;
            }
            return value.doubleValue();
        } else {
            return null;
        }
    }

So it does not apply the checks to the BigDecimal derived string

Why not do

private Double convertImpl(Object input) {
        // From the GraphQL Float spec, non-finite floating-point internal values (NaN and Infinity)
        // must raise a field error on both result and input coercion
        Double doubleInput;
        if (input instanceof Double) {
            doubleInput = (Double) input;
        } else if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                return null;
            }
            doubleInput =  value.doubleValue();
        } else {
            return null;
        }
        
        if (Double.isNaN(doubleInput) || Double.isInfinite(doubleInput)) {
                return null;
        }
       return doubleInput
    }

Forgive me if this doesnt compile

return null;
}
return doubleInput;
} else if (isNumberIsh(input)) {
Copy link
Member

Choose a reason for hiding this comment

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

This seems more sensible - eg first get it into a double and then check it - regardless

Even if BigDecimal never produces Nan or infinity, the logic reads better

Double.NEGATIVE_INFINITY | _
Float.NaN | _
Float.POSITIVE_INFINITY | _
Float.NEGATIVE_INFINITY | _
Copy link
Member

Choose a reason for hiding this comment

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

Can we have a string rep here? Float.NEGATIVE_INFINITY.toString() ?

Copy link
Member

Choose a reason for hiding this comment

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

 public static void main(String[] args) {
        String negInf = String.valueOf(Float.NEGATIVE_INFINITY);
        System.out.println(negInf);
        double dv = new BigDecimal(negInf).doubleValue();
        System.out.println(dv);
    }

-Infinity
Exception in thread "main" java.lang.NumberFormatException
	at java.math.BigDecimal.<init>(BigDecimal.java:494)
	at java.math.BigDecimal.<init>(BigDecimal.java:383)
	at java.math.BigDecimal.<init>(BigDecimal.java:806)

So its not acceptable but lets test anyway to show that we handle it

final Object serialized = serializeLegacy(type, value, graphqlContext, locale);
if (isNullishLegacy(serialized)) {
return null;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This NaN check was unnecessary. The NaN check already happened inside the serialize method.

if (Double.isNaN(doubleInput) || Double.isInfinite(doubleInput)) {
return null;
}
return doubleInput;
Copy link
Member Author

Choose a reason for hiding this comment

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

Move NaN/Infinity check lower down to make this easier to read

Copy link
Member Author

Choose a reason for hiding this comment

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

Edit: not only easier to read, it catches the edge case where value.doubleValue() returns positive/negative infinity

Float.POSITIVE_INFINITY | _
Float.POSITIVE_INFINITY.toString() | _
Float.NEGATIVE_INFINITY | _
Float.NEGATIVE_INFINITY.toString() | _
Copy link
Member Author

Choose a reason for hiding this comment

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

Add String representations to test

@@ -1,6 +1,13 @@
package graphql.language
package graphql.execution
Copy link
Member Author

@dondonz dondonz Oct 16, 2022

Choose a reason for hiding this comment

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

Tidy up package name and constructor usage

@dondonz dondonz requested a review from bbakerman October 16, 2022 21:11
Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

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

Merge away

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants