Skip to content

Conversation

@mk868
Copy link
Contributor

@mk868 mk868 commented Apr 6, 2025

Related: #3878

I applied @NullMarked and @Nullable annotations to these classes:

  • graphql.language.Node - partially annotated with @Nullable
  • graphql.language.AbstractNode
  • graphql.language.ArrayValue
  • graphql.language.BooleanValue
  • graphql.language.EnumValue
  • graphql.language.FloatValue
  • graphql.language.IntValue
  • graphql.language.NullValue
  • graphql.language.ObjectValue
  • graphql.language.ScalarValue
  • graphql.language.StringValue - I was not sure about the nullability of value, for now I marked it @Nullable.
  • graphql.language.Value
  • graphql.language.VariableReference

Notes

  • SourceLocation sourceLocation - can contain a null value in all places

Potential problems (will be pointed by #3852 )

  • EnumValue.Builder#build() can build an EnumValue object with a null name
  • FloatValue.Builder#build() can build an FloatValue object with a null value
  • IntValue.Builder#build() can build an IntValue object with a null value
  • VariableReference.Builder#build() can build an VariableReference object with a null name

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR applies nullability annotations across the graphql.language package to clarify which fields and parameters can be null.

  • Added @NullMarked and @nullable annotations to key classes and constructors.
  • Updated Builder classes for several Value types to accept null parameters.
  • Adjusted method signatures (e.g. isEqualTo) and field declarations to reflect nullability.

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
VariableReference.java Annotated the class and its Builder with nullability hints.
Value.java Marked the interface with @NullMarked.
StringValue.java Adjusted the field and getter for value to be @nullable.
ScalarValue.java Applied @NullMarked to the interface.
ObjectValue.java Annotated constructors and Builder methods with @nullable.
NullValue.java Updated constructor and Builder to support null SourceLocation.
Node.java Updated getSourceLocation and isEqualTo for nullability.
IntValue.java, FloatValue.java, EnumValue.java, BooleanValue.java, ArrayValue.java, AbstractNode.java Applied similar nullability annotations to constructors, methods, and Builder classes.

private @Nullable SourceLocation sourceLocation;
private ImmutableList<Comment> comments = emptyList();
private String name;
private @Nullable String name;
Copy link

Copilot AI May 11, 2025

Choose a reason for hiding this comment

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

The VariableReference.Builder now allows a null name. Consider adding validation in the build() method to ensure that a non-null name is provided if that is an invariant in your domain.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

name in the above constructor and builder method are not nullable, did you meant to make this field nullable?

I think in practice, a VariableReference should always have a non-nullable name

private SourceLocation sourceLocation;
private String name;
private @Nullable SourceLocation sourceLocation;
private @Nullable String name;
Copy link

Copilot AI May 11, 2025

Choose a reason for hiding this comment

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

The EnumValue.Builder permits a null name. It may be beneficial to enforce a non-null name during object construction if that is required by the API contract.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Hello, thanks for your patience while away for Easter.

In this builder, name is set to nullable, but the constructor above has name as non-nullable, and the builder method below has it as non-nullable. Did you mean for the Builder's name to be instead non-nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, nullable fields in the builder are expected.
By default, when a builder instance is created, all values are set to their defaults (null for reference fields) - thus, it's necessary to mark these fields @Nullable.
This initial Builder state represents an incomplete configuration, and the EnumValue should not be constructible in this state - calling the build() method should result in an IllegalStateException.

I planned to add something like this later:

public EnumValue build() {
    if (name == null) {
        throw new IllegalStateException("name must be set");
    }
    return new EnumValue(name, sourceLocation, comments, ignoredChars, additionalData);
}

private SourceLocation sourceLocation;
private BigInteger value;
private @Nullable SourceLocation sourceLocation;
private @Nullable BigInteger value;
Copy link

Copilot AI May 11, 2025

Choose a reason for hiding this comment

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

The IntValue.Builder now accepts a null value. Consider adding a check in the build() method to prevent creating an IntValue instance with a null value if that contradicts expected behavior.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as for FloatValue, I will double check if a null value is in practice possible or not

private SourceLocation sourceLocation;
private BigDecimal value;
private @Nullable SourceLocation sourceLocation;
private @Nullable BigDecimal value;
Copy link

Copilot AI May 11, 2025

Choose a reason for hiding this comment

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

The FloatValue.Builder now permits a null value. If a non-null value is expected, it would be prudent to validate this in the build() method to prevent possible misuse, as noted in the PR description.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

This builder should be consistent with the constructor for value

@andimarek @bbakerman: is it possible in practice for FloatValue to ever be constructed with a null? Or would such an object always be constructed as a NullValue?

@dondonz
Copy link
Member

dondonz commented May 11, 2025

Giving Copilot a try for fun, I'm curious what it says

private SourceLocation sourceLocation;
private String value;
private @Nullable SourceLocation sourceLocation;
private @Nullable String value;
Copy link
Member

Choose a reason for hiding this comment

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

Similar to above Value classes, I will double check if value can be nullable or if in practice it is represented by a NullValue

@dondonz
Copy link
Member

dondonz commented Jun 12, 2025

Coming back in here to say thanks for waiting and thanks for the PR. We have a new PR up to add ErrorProne & NullAway checks which I'd like to merge in before new JSpecify PRs

public ArrayValue deepCopy() {
return new ArrayValue(deepCopy(values), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData());
List<Value> copiedValues = deepCopy(values);
return new ArrayValue(copiedValues != null ? copiedValues : emptyList(), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData());
Copy link
Member

Choose a reason for hiding this comment

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

Want to call out this is a small change to not allow a null value. A list will be used instead. Without this change, there is a NullAway error

return builder.build();
}

@NullUnmarked
Copy link
Member

Choose a reason for hiding this comment

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

After giving a few classes the JSpecify treatment: we now generally annotate Builders with @NullUnmarked

public ObjectValue deepCopy() {
return new ObjectValue(deepCopy(objectFields), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData());
List<ObjectField> copiedFields = deepCopy(objectFields);
return new ObjectValue(copiedFields != null ? copiedFields : emptyList(), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData());
Copy link
Member

Choose a reason for hiding this comment

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

Similar to ArrayValue, want to call out this is a small change to not allow a null value. A list will be used instead. Without this change, there is a NullAway error

@dondonz
Copy link
Member

dondonz commented Aug 3, 2025

Hello after a few other PRs with JSpecify annotations, we settled on using @NullUnmarked for Builders as they can be instantiated with null variables, which are later set to non-null values. I updated your PR.

We've also since added NullAway compile time checks which identified a very small extra change, to make the values of both ArrayValue and ObjectValue empty lists rather than null.

Thanks for the PR!


@SuppressWarnings("unchecked")
protected <V extends Node> V deepCopy(V nullableObj) {
protected <V extends @Nullable Node> @Nullable V deepCopy(@Nullable V nullableObj) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this change @dondonz ... why should we allow null values here?

Copy link
Member

Choose a reason for hiding this comment

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

The first line we check whether the input is nullable if (nullableObj == null) { so I have allowed the input to be nullable

I agree it's strange in the context of Value like FloatValue.

Maybe there was another reason to allow this to be nullable in the first place?

Copy link
Member

Choose a reason for hiding this comment

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

So we have two options here: either declare V as a type that also can be null or declare input and output as Nullable. We have done both here, which doesn't make sense I think.

Copy link
Member

Choose a reason for hiding this comment

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

Repeating conversation here: I will change the method signature to become

protected <V extends Node> @Nullable V deepCopy(@Nullable V nullableObj)

This in English is meant to express that V extending Node is not nullable, but because the method will handle null inputs, it may still return a null

Copy link
Member

@andimarek andimarek left a comment

Choose a reason for hiding this comment

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

Can be merged, but also could first fix the small comment I left.


@SuppressWarnings("unchecked")
protected <V extends Node> V deepCopy(V nullableObj) {
protected <V extends @Nullable Node> @Nullable V deepCopy(@Nullable V nullableObj) {
Copy link
Member

Choose a reason for hiding this comment

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

So we have two options here: either declare V as a type that also can be null or declare input and output as Nullable. We have done both here, which doesn't make sense I think.

@Override
public ArrayValue deepCopy() {
return new ArrayValue(deepCopy(values), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData());
List<Value> copiedValues = assertNotNull(deepCopy(values));
Copy link
Member

Choose a reason for hiding this comment

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

Add an assert here to keep NullAway happy

In practice it is not possible to have null values. Have to put this here otherwise you'll get a NullAway error

@Override
public ObjectValue deepCopy() {
return new ObjectValue(deepCopy(objectFields), getSourceLocation(), getComments(), getIgnoredChars(), getAdditionalData());
List<ObjectField> copiedFields = assertNotNull(deepCopy(objectFields));
Copy link
Member

Choose a reason for hiding this comment

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

Add this assert to keep NullAway happy, even though objectFields is not null

@dondonz dondonz merged commit fc171b1 into graphql-java:master Aug 10, 2025
1 check passed
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