Skip to content

Conversation

@dondonz
Copy link
Member

@dondonz dondonz commented Dec 4, 2025

We have many classes left to go for JSpecify, so rather than one class at a time I will do the remainder in large bundles. This adds annotations on ~50 classes.

The general recipe here is

  • Use IntelliJ's infer nullity tool, which is a start and covers the basics
  • AI prompt to read JavaDoc, inspect nearby methods, with self-verification using NullAway
  • And finally a manual check from me, as some items are hard to infer nullity without understanding the GraphQL specification or the codebase

What will be in a future PR: better Kotlin tooling as reported recently in #4179

@dondonz dondonz marked this pull request as draft December 4, 2025 03:27
ParseAndValidateResult parseResult = parse(executionInput, graphQLSchema, instrumentationState);
if (parseResult.isFailure()) {
return new PreparsedDocumentEntry(parseResult.getSyntaxException().toInvalidSyntaxError());
return new PreparsedDocumentEntry(assertNotNull(parseResult.getSyntaxException(), "Parse result syntax exception cannot be null when failed").toInvalidSyntaxError());
Copy link
Member Author

Choose a reason for hiding this comment

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

A similar change now has to happen in a bunch of places: now we must be asserting not null on selected lines or else you'll get a NullAway compile warning because the nullity is not clear at this point in the execution


Predicate<Class<?>> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true);
Locale locale = executionInput.getLocale() != null ? executionInput.getLocale() : Locale.getDefault();
Locale locale = executionInput.getLocale();
Copy link
Member Author

Choose a reason for hiding this comment

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

And now there are IDE alerts of null checks we can remove. When we are done with all JSpecify annotations on public API classes, we will find many more null checks to remove

* @return the parsed document or null if it's syntactically invalid.
*/
public Document getDocument() {
public @Nullable Document getDocument() {
Copy link
Member Author

Choose a reason for hiding this comment

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

Inferred from the Javadoc

private final Map<String, Object> arguments;

public FieldComplexityEnvironment(Field field, GraphQLFieldDefinition fieldDefinition, GraphQLCompositeType parentType, Map<String, Object> arguments, FieldComplexityEnvironment parentEnvironment) {
public FieldComplexityEnvironment(Field field, GraphQLFieldDefinition fieldDefinition, GraphQLCompositeType parentType, Map<String, Object> arguments, @Nullable FieldComplexityEnvironment parentEnvironment) {
Copy link
Member Author

@dondonz dondonz Dec 4, 2025

Choose a reason for hiding this comment

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

It can be nullable because a method that calls this constructor may pass in a null value for parentEnvironment


@Override
public @Nullable CompletableFuture<InstrumentationState> createStateAsync(InstrumentationCreateStateParameters parameters) {
public CompletableFuture<InstrumentationState> createStateAsync(InstrumentationCreateStateParameters parameters) {
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 think these @Nullable annotations were not correct initially. This method always will return a completed future


@Override
public @Nullable InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState rawState) {
public InstrumentationContext<List<ValidationError>> beginValidation(InstrumentationValidationParameters parameters, InstrumentationState rawState) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above, I think this was incorrectly marked as nullable initially


@Override
public @Nullable InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters instrumentationExecuteOperationParameters, InstrumentationState rawState) {
public InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters instrumentationExecuteOperationParameters, InstrumentationState rawState) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above, I think this was incorrectly marked as nullable initially


@Override
public @Nullable InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
public InstrumentationContext<ExecutionResult> beginExecuteOperation(InstrumentationExecuteOperationParameters parameters, InstrumentationState state) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Same as above, I think this was incorrectly marked as nullable initially


/**
* @return a list of {@link graphql.relay.Edge}s that are really a node of data and its cursor
* @return a list of {@link graphql.relay.Edge}s that contain a node of data and its cursor. Can be null as defined in the spec.
Copy link
Member Author

Choose a reason for hiding this comment

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

From the specification


public DefaultConnectionCursor(String value) {
Assert.assertTrue(value != null && !value.isEmpty(), "connection value cannot be null or empty");
Assert.assertTrue(!value.isEmpty(), "connection value cannot be null or empty");
Copy link
Member Author

Choose a reason for hiding this comment

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

Remove null check

import java.util.Objects;

@PublicApi
@NullMarked
Copy link
Member Author

Choose a reason for hiding this comment

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

A wider question: these Relay classes are quite old and never got much love. Do we want to retain these classes at all?

@Internal
private GraphQLScalarType(String name,
String description,
@Nullable String description,
Copy link
Member Author

Choose a reason for hiding this comment

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

By definition in the specification, the description, and specified by URL can be null.

private final ImmutableList<SchemaExtensionDefinition> extensionDefinitions;
private final String description;
private final GraphQLCodeRegistry codeRegistry;
private final @Nullable GraphQLCodeRegistry codeRegistry;
Copy link
Member Author

Choose a reason for hiding this comment

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

See previous discussion in #4098 (comment) (this PR supercedes the older one)

Copy link
Member Author

Choose a reason for hiding this comment

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

If we want to remove the nullable annotation here, we'd also need to change another callsite that assumes this parameter to be null

private final String propertyName;
private final Function<Object, Object> function;
private final @Nullable String propertyName;
private final @Nullable Function<Object, Object> function;
Copy link
Member Author

Choose a reason for hiding this comment

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

See the constructors below, it is possible for either field to be nullable

return NonNullType.newNonNullType(replaceTypeName(((NonNullType) type).getType(), newName)).build();
} else {
graphql.Assert.assertShouldNeverHappen();
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 throws, so the null is never reached

"graphql.validation.ValidationError",
"graphql.validation.ValidationErrorClassification",
"graphql.validation.ValidationErrorType",
// These classes will not be public API later, exempt here while marked as experimental
Copy link
Member Author

Choose a reason for hiding this comment

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

There is an edge case where some classes have been annotated with "Experimental API" for the purposes of the defer work. They won't become public API later so I made a note in the JSpecify exemption rule

private final List<SourceLocation> locations;
private final Map<String, Object> extensions;
private final List<Object> path;
private final @Nullable List<SourceLocation> locations;
Copy link
Member Author

@dondonz dondonz Dec 7, 2025

Choose a reason for hiding this comment

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

The builder creates a non-nullable list, but that's only if the builder is called

@dondonz dondonz marked this pull request as ready for review December 7, 2025 04:13
* @return a value or null
*/
public <T> T get(Object key) {
public <T> @Nullable T get(Object key) {
Copy link
Member Author

Choose a reason for hiding this comment

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

TODO: another case of the nullable generic issue

*/
@SuppressWarnings("unchecked")
public <T> T getObject() {
public @Nullable <T> T getObject() {
Copy link
Member Author

Choose a reason for hiding this comment

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

TODO: another generic nullable check

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.

2 participants