diff --git a/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy new file mode 100644 index 0000000000..ee8e4aec58 --- /dev/null +++ b/src/test/groovy/graphql/archunit/JSpecifyAnnotationsCheck.groovy @@ -0,0 +1,407 @@ +package graphql + +import com.tngtech.archunit.core.importer.ClassFileImporter +import com.tngtech.archunit.core.importer.ImportOption +import spock.lang.Specification + +/** + * This test ensures that all public API and experimental API classes + * are properly annotated with JSpecify annotations. + */ +class JSpecifyAnnotationsCheck extends Specification { + + private static final Set JSPECIFY_EXEMPTION_LIST = [ + "graphql.AssertException", + "graphql.Directives", + "graphql.ErrorClassification", + "graphql.ErrorType", + "graphql.ExceptionWhileDataFetching", + "graphql.ExecutionResult", + "graphql.GraphQLContext", + "graphql.GraphQLError", + "graphql.GraphqlErrorBuilder", + "graphql.GraphqlErrorException", + "graphql.ParseAndValidate", + "graphql.ParseAndValidateResult", + "graphql.Scalars", + "graphql.SerializationError", + "graphql.TypeMismatchError", + "graphql.TypeResolutionEnvironment", + "graphql.UnresolvedTypeError", + "graphql.agent.result.ExecutionTrackingResult", + "graphql.analysis.FieldComplexityCalculator", + "graphql.analysis.FieldComplexityEnvironment", + "graphql.analysis.MaxQueryComplexityInstrumentation", + "graphql.analysis.MaxQueryDepthInstrumentation", + "graphql.analysis.QueryComplexityCalculator", + "graphql.analysis.QueryComplexityInfo", + "graphql.analysis.QueryComplexityInfo\$Builder", + "graphql.analysis.QueryDepthInfo", + "graphql.analysis.QueryDepthInfo\$Builder", + "graphql.analysis.QueryReducer", + "graphql.analysis.QueryTransformer", + "graphql.analysis.QueryTransformer\$Builder", + "graphql.analysis.QueryTraversalOptions", + "graphql.analysis.QueryTraverser", + "graphql.analysis.QueryTraverser\$Builder", + "graphql.analysis.QueryVisitor", + "graphql.analysis.QueryVisitorFieldArgumentEnvironment", + "graphql.analysis.QueryVisitorFieldArgumentInputValue", + "graphql.analysis.QueryVisitorFieldArgumentValueEnvironment", + "graphql.analysis.QueryVisitorFieldEnvironment", + "graphql.analysis.QueryVisitorFragmentDefinitionEnvironment", + "graphql.analysis.QueryVisitorFragmentSpreadEnvironment", + "graphql.analysis.QueryVisitorInlineFragmentEnvironment", + "graphql.analysis.QueryVisitorStub", + "graphql.analysis.values.ValueTraverser", + "graphql.execution.AbortExecutionException", + "graphql.execution.AsyncExecutionStrategy", + "graphql.execution.AsyncSerialExecutionStrategy", + "graphql.execution.CoercedVariables", + "graphql.execution.DataFetcherExceptionHandlerParameters", + "graphql.execution.DataFetcherExceptionHandlerResult", + "graphql.execution.DefaultValueUnboxer", + "graphql.execution.ExecutionContext", + "graphql.execution.ExecutionId", + "graphql.execution.ExecutionStepInfo", + "graphql.execution.ExecutionStrategyParameters", + "graphql.execution.FetchedValue", + "graphql.execution.FieldValueInfo", + "graphql.execution.InputMapDefinesTooManyFieldsException", + "graphql.execution.MergedSelectionSet", + "graphql.execution.MissingRootTypeException", + "graphql.execution.NonNullableValueCoercedAsNullException", + "graphql.execution.NormalizedVariables", + "graphql.execution.OneOfNullValueException", + "graphql.execution.OneOfTooManyKeysException", + "graphql.execution.ResultNodesInfo", + "graphql.execution.ResultPath", + "graphql.execution.SimpleDataFetcherExceptionHandler", + "graphql.execution.SubscriptionExecutionStrategy", + "graphql.execution.UnknownOperationException", + "graphql.execution.UnresolvedTypeException", + "graphql.execution.conditional.ConditionalNodeDecision", + "graphql.execution.directives.QueryAppliedDirective", + "graphql.execution.directives.QueryAppliedDirectiveArgument", + "graphql.execution.directives.QueryDirectives", + "graphql.execution.incremental.DeferredExecution", + "graphql.execution.instrumentation.ChainedInstrumentation", + "graphql.execution.instrumentation.DocumentAndVariables", + "graphql.execution.instrumentation.NoContextChainedInstrumentation", + "graphql.execution.ResponseMapFactory", + "graphql.execution.instrumentation.SimpleInstrumentation", + "graphql.execution.instrumentation.SimpleInstrumentationContext", + "graphql.execution.instrumentation.SimplePerformantInstrumentation", + "graphql.execution.instrumentation.fieldvalidation.FieldAndArguments", + "graphql.execution.instrumentation.fieldvalidation.FieldValidationEnvironment", + "graphql.execution.instrumentation.fieldvalidation.FieldValidationInstrumentation", + "graphql.execution.instrumentation.fieldvalidation.SimpleFieldValidation", + "graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters", + "graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters", + "graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters", + "graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters", + "graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters", + "graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters", + "graphql.execution.instrumentation.parameters.InstrumentationFieldParameters", + "graphql.execution.instrumentation.parameters.InstrumentationValidationParameters", + "graphql.execution.instrumentation.tracing.TracingInstrumentation", + "graphql.execution.instrumentation.tracing.TracingSupport", + "graphql.execution.preparsed.PreparsedDocumentEntry", + "graphql.execution.preparsed.persisted.ApolloPersistedQuerySupport", + "graphql.execution.preparsed.persisted.InMemoryPersistedQueryCache", + "graphql.execution.preparsed.persisted.PersistedQueryCacheMiss", + "graphql.execution.preparsed.persisted.PersistedQueryIdInvalid", + "graphql.execution.preparsed.persisted.PersistedQueryNotFound", + "graphql.execution.reactive.DelegatingSubscription", + "graphql.execution.reactive.SubscriptionPublisher", + "graphql.extensions.ExtensionsBuilder", + "graphql.incremental.DeferPayload", + "graphql.incremental.DelayedIncrementalPartialResult", + "graphql.incremental.DelayedIncrementalPartialResultImpl", + "graphql.incremental.IncrementalExecutionResult", + "graphql.incremental.IncrementalExecutionResultImpl", + "graphql.incremental.IncrementalPayload", + "graphql.incremental.StreamPayload", + "graphql.introspection.GoodFaithIntrospection", + "graphql.introspection.Introspection", + "graphql.introspection.IntrospectionQuery", + "graphql.introspection.IntrospectionQueryBuilder", + "graphql.introspection.IntrospectionResultToSchema", + "graphql.introspection.IntrospectionWithDirectivesSupport", + "graphql.introspection.IntrospectionWithDirectivesSupport\$DirectivePredicateEnvironment", + "graphql.language.AbstractDescribedNode", + "graphql.language.AbstractNode", + "graphql.language.Argument", + "graphql.language.ArrayValue", + "graphql.language.AstNodeAdapter", + "graphql.language.AstPrinter", + "graphql.language.AstSignature", + "graphql.language.AstSorter", + "graphql.language.AstTransformer", + "graphql.language.BooleanValue", + "graphql.language.Comment", + "graphql.language.Definition", + "graphql.language.DescribedNode", + "graphql.language.Description", + "graphql.language.Directive", + "graphql.language.DirectiveDefinition", + "graphql.language.DirectiveLocation", + "graphql.language.DirectivesContainer", + "graphql.language.Document", + "graphql.language.EnumTypeDefinition", + "graphql.language.EnumTypeExtensionDefinition", + "graphql.language.EnumValue", + "graphql.language.EnumValueDefinition", + "graphql.language.Field", + "graphql.language.FieldDefinition", + "graphql.language.FloatValue", + "graphql.language.FragmentDefinition", + "graphql.language.FragmentSpread", + "graphql.language.IgnoredChar", + "graphql.language.IgnoredChars", + "graphql.language.ImplementingTypeDefinition", + "graphql.language.InlineFragment", + "graphql.language.InputObjectTypeDefinition", + "graphql.language.InputObjectTypeExtensionDefinition", + "graphql.language.InputValueDefinition", + "graphql.language.IntValue", + "graphql.language.InterfaceTypeDefinition", + "graphql.language.InterfaceTypeExtensionDefinition", + "graphql.language.ListType", + "graphql.language.NamedNode", + "graphql.language.Node", + "graphql.language.NodeBuilder", + "graphql.language.NodeChildrenContainer", + "graphql.language.NodeDirectivesBuilder", + "graphql.language.NodeParentTree", + "graphql.language.NodeTraverser", + "graphql.language.NodeVisitor", + "graphql.language.NodeVisitorStub", + "graphql.language.NonNullType", + "graphql.language.NullValue", + "graphql.language.ObjectField", + "graphql.language.ObjectTypeDefinition", + "graphql.language.ObjectTypeExtensionDefinition", + "graphql.language.ObjectValue", + "graphql.language.OperationDefinition", + "graphql.language.OperationTypeDefinition", + "graphql.language.PrettyAstPrinter", + "graphql.language.SDLDefinition", + "graphql.language.SDLExtensionDefinition", + "graphql.language.SDLNamedDefinition", + "graphql.language.ScalarTypeDefinition", + "graphql.language.ScalarTypeExtensionDefinition", + "graphql.language.ScalarValue", + "graphql.language.SchemaDefinition", + "graphql.language.SchemaExtensionDefinition", + "graphql.language.Selection", + "graphql.language.SelectionSet", + "graphql.language.SelectionSetContainer", + "graphql.language.SourceLocation", + "graphql.language.StringValue", + "graphql.language.Type", + "graphql.language.TypeDefinition", + "graphql.language.TypeKind", + "graphql.language.TypeName", + "graphql.language.UnionTypeDefinition", + "graphql.language.UnionTypeExtensionDefinition", + "graphql.language.Value", + "graphql.language.VariableDefinition", + "graphql.language.VariableReference", + "graphql.normalized.ExecutableNormalizedField", + "graphql.normalized.ExecutableNormalizedOperation", + "graphql.normalized.ExecutableNormalizedOperationFactory", + "graphql.normalized.ExecutableNormalizedOperationToAstCompiler", + "graphql.normalized.NormalizedInputValue", + "graphql.normalized.incremental.NormalizedDeferredExecution", + "graphql.normalized.nf.NormalizedDocument", + "graphql.normalized.nf.NormalizedDocumentFactory", + "graphql.normalized.nf.NormalizedField", + "graphql.normalized.nf.NormalizedOperation", + "graphql.normalized.nf.NormalizedOperationToAstCompiler", + "graphql.parser.InvalidSyntaxException", + "graphql.parser.MultiSourceReader", + "graphql.parser.Parser", + "graphql.parser.ParserEnvironment", + "graphql.parser.ParserOptions", + "graphql.relay.Connection", + "graphql.relay.ConnectionCursor", + "graphql.relay.DefaultConnection", + "graphql.relay.DefaultConnectionCursor", + "graphql.relay.DefaultEdge", + "graphql.relay.DefaultPageInfo", + "graphql.relay.Edge", + "graphql.relay.PageInfo", + "graphql.relay.Relay", + "graphql.relay.SimpleListConnection", + "graphql.schema.AsyncDataFetcher", + "graphql.schema.CoercingParseLiteralException", + "graphql.schema.CoercingParseValueException", + "graphql.schema.CoercingSerializeException", + "graphql.schema.DataFetcherFactories", + "graphql.schema.DataFetcherFactoryEnvironment", + "graphql.schema.DataFetchingFieldSelectionSet", + "graphql.schema.DefaultGraphqlTypeComparatorRegistry", + "graphql.schema.DelegatingDataFetchingEnvironment", + "graphql.schema.FieldCoordinates", + "graphql.schema.GraphQLAppliedDirective", + "graphql.schema.GraphQLAppliedDirectiveArgument", + "graphql.schema.GraphQLArgument", + "graphql.schema.GraphQLCodeRegistry", + "graphql.schema.GraphQLCompositeType", + "graphql.schema.GraphQLDirective", + "graphql.schema.GraphQLDirectiveContainer", + "graphql.schema.GraphQLEnumType", + "graphql.schema.GraphQLEnumValueDefinition", + "graphql.schema.GraphQLEnumValueDefinition\$Builder", + "graphql.schema.GraphQLFieldDefinition", + "graphql.schema.GraphQLFieldDefinition\$Builder", + "graphql.schema.GraphQLFieldsContainer", + "graphql.schema.GraphQLImplementingType", + "graphql.schema.GraphQLInputFieldsContainer", + "graphql.schema.GraphQLInputObjectField", + "graphql.schema.GraphQLInputObjectField\$Builder", + "graphql.schema.GraphQLInputObjectType", + "graphql.schema.GraphQLInputObjectType\$Builder", + "graphql.schema.GraphQLInputSchemaElement", + "graphql.schema.GraphQLInputType", + "graphql.schema.GraphQLInputValueDefinition", + "graphql.schema.GraphQLInterfaceType", + "graphql.schema.GraphQLInterfaceType\$Builder", + "graphql.schema.GraphQLList", + "graphql.schema.GraphQLModifiedType", + "graphql.schema.GraphQLNamedInputType", + "graphql.schema.GraphQLNamedOutputType", + "graphql.schema.GraphQLNamedSchemaElement", + "graphql.schema.GraphQLNamedType", + "graphql.schema.GraphQLNonNull", + "graphql.schema.GraphQLNullableType", + "graphql.schema.GraphQLObjectType", + "graphql.schema.GraphQLObjectType\$Builder", + "graphql.schema.GraphQLOutputType", + "graphql.schema.GraphQLScalarType", + "graphql.schema.GraphQLScalarType\$Builder", + "graphql.schema.GraphQLSchema", + "graphql.schema.GraphQLSchemaElement", + "graphql.schema.GraphQLType", + "graphql.schema.GraphQLTypeReference", + "graphql.schema.GraphQLTypeUtil", + "graphql.schema.GraphQLTypeVisitor", + "graphql.schema.GraphQLTypeVisitorStub", + "graphql.schema.GraphQLUnionType", + "graphql.schema.GraphQLUnionType\$Builder", + "graphql.schema.GraphQLUnmodifiedType", + "graphql.schema.GraphqlElementParentTree", + "graphql.schema.GraphqlTypeComparatorEnvironment", + "graphql.schema.GraphqlTypeComparatorRegistry", + "graphql.schema.InputValueWithState", + "graphql.schema.PropertyDataFetcher", + "graphql.schema.SchemaElementChildrenContainer", + "graphql.schema.SchemaTransformer", + "graphql.schema.SchemaTraverser", + "graphql.schema.SelectedField", + "graphql.schema.StaticDataFetcher", + "graphql.schema.diff.DiffCategory", + "graphql.schema.diff.DiffEvent", + "graphql.schema.diff.DiffLevel", + "graphql.schema.diff.DiffSet", + "graphql.schema.diff.SchemaDiffSet", + "graphql.schema.diff.reporting.CapturingReporter", + "graphql.schema.diff.reporting.ChainedReporter", + "graphql.schema.diff.reporting.PrintStreamReporter", + "graphql.schema.diffing.SchemaGraph", + "graphql.schema.idl.CombinedWiringFactory", + "graphql.schema.idl.DirectiveInfo", + "graphql.schema.idl.MapEnumValuesProvider", + "graphql.schema.idl.NaturalEnumValuesProvider", + "graphql.schema.idl.RuntimeWiring", + "graphql.schema.idl.RuntimeWiring\$Builder", + "graphql.schema.idl.SchemaDirectiveWiring", + "graphql.schema.idl.SchemaDirectiveWiringEnvironment", + "graphql.schema.idl.SchemaGenerator", + "graphql.schema.idl.SchemaPrinter", + "graphql.schema.idl.TypeRuntimeWiring", + "graphql.schema.idl.errors.SchemaProblem", + "graphql.schema.idl.errors.StrictModeWiringException", + "graphql.schema.transform.FieldVisibilitySchemaTransformation", + "graphql.schema.transform.VisibleFieldPredicateEnvironment", + "graphql.schema.usage.SchemaUsage", + "graphql.schema.usage.SchemaUsageSupport", + "graphql.schema.validation.OneOfInputObjectRules", + "graphql.schema.validation.SchemaValidationErrorClassification", + "graphql.schema.visibility.BlockedFields", + "graphql.schema.visibility.DefaultGraphqlFieldVisibility", + "graphql.schema.visibility.GraphqlFieldVisibility", + "graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility", + "graphql.schema.visitor.GraphQLSchemaTraversalControl", + "graphql.util.Anonymizer", + "graphql.util.Breadcrumb", + "graphql.util.CyclicSchemaAnalyzer", + "graphql.util.NodeAdapter", + "graphql.util.NodeLocation", + "graphql.util.NodeMultiZipper", + "graphql.util.NodeZipper", + "graphql.util.querygenerator.QueryGenerator", + "graphql.util.querygenerator.QueryGeneratorOptions", + "graphql.util.querygenerator.QueryGeneratorOptions\$QueryGeneratorOptionsBuilder", + "graphql.util.querygenerator.QueryGeneratorResult", + "graphql.util.TraversalControl", + "graphql.util.TraverserContext", + "graphql.util.TreeTransformer", + "graphql.util.TreeTransformerUtil", + "graphql.validation.ValidationError", + "graphql.validation.ValidationErrorClassification", + "graphql.validation.ValidationErrorType", + "graphql.validation.rules.DeferDirectiveLabel", + "graphql.validation.rules.DeferDirectiveOnRootLevel", + "graphql.validation.rules.DeferDirectiveOnValidOperation" + ] as Set + + def "ensure all public API and experimental API classes have @NullMarked annotation"() { + given: + def classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages("graphql") + .stream() + .filter { it.isAnnotatedWith("graphql.PublicApi") || it.isAnnotatedWith("graphql.ExperimentalApi") } + .collect() + + when: + def classesMissingAnnotation = classes + .stream() + .filter { !it.isAnnotatedWith("org.jspecify.annotations.NullMarked") && !it.isAnnotatedWith("org.jspecify.annotations.NullUnmarked") } + .map { it.name } + .filter { it -> !JSPECIFY_EXEMPTION_LIST.contains(it) } + .collect() + + then: + if (!classesMissingAnnotation.isEmpty()) { + throw new AssertionError("""The following public API and experimental API classes are missing a JSpecify annotation: +${classesMissingAnnotation.sort().join("\n")} + +Add @NullMarked or @NullUnmarked to these public API classes. See documentation at https://jspecify.dev/docs/user-guide/#nullmarked""") + } + } + + def "exempted classes should not be annotated with @NullMarked or @NullUnmarked"() { + given: + def classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages("graphql") + + when: + def annotatedButExempted = classes.stream() + .filter { JSPECIFY_EXEMPTION_LIST.contains(it.name) } + .filter { it.isAnnotatedWith("org.jspecify.annotations.NullMarked") || it.isAnnotatedWith("org.jspecify.annotations.NullUnmarked") } + .map { it.name } + .collect() + + then: + if (!annotatedButExempted.isEmpty()) { + throw new AssertionError("""The following classes are in the JSpecify exemption list but are annotated with @NullMarked or @NullUnmarked: +${annotatedButExempted.sort().join("\n")} + +Please remove them from the exemption list in ${JSpecifyAnnotationsCheck.class.simpleName}.groovy.""") + } + } +} \ No newline at end of file