Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input deprecation tracker #1064

Merged
merged 3 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,53 @@ lazy val core = project
ProblemFilters.exclude[DirectMissingMethodProblem]("sangria.schema.InputField.this"),
ProblemFilters.exclude[DirectMissingMethodProblem]("sangria.schema.InputField.apply"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.schema.InputValue.deprecationReason")
"sangria.schema.InputValue.deprecationReason"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.execution.DeprecationTracker.deprecatedDirectiveArgUsed"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.execution.DeprecationTracker.deprecatedInputObjectFieldUsed"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.execution.DeprecationTracker.deprecatedFieldArgUsed"),
ProblemFilters.exclude[DirectMissingMethodProblem](
"sangria.execution.DeprecationTracker.empty"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.<init>$default$5"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.execute$default$10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.prepare$default$10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.deprecationTracker"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.copy$default$5"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.Executor.apply$default$5"),
ProblemFilters.exclude[IncompatibleResultTypeProblem]("sangria.execution.Executor._5"),
ProblemFilters.exclude[MissingClassProblem]("sangria.execution.NilDeprecationTracker"),
ProblemFilters.exclude[MissingClassProblem]("sangria.execution.NilDeprecationTracker$"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.QueryReducerExecutor.reduceQueryWithoutVariables$default$8"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.ValueCoercionHelper.<init>$default$2"),
ProblemFilters.exclude[IncompatibleMethTypeProblem](
"sangria.execution.ValueCoercionHelper.this"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.ValueCoercionHelper.<init>$default$2"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.execution.ValueCollector.this"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.execution.batch.BatchExecutor.executeBatch$default$10"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.schema.Context.apply"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.schema.Context.this"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.schema.Context.deprecationTracker"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("sangria.schema.Context.copy"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.schema.Context.copy$default$10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem]("sangria.schema.Context._10"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"sangria.schema.WithInputTypeRendering.deprecationTracker"),
ProblemFilters.exclude[ReversedMissingMethodProblem](
"sangria.schema.WithInputTypeRendering.deprecationTracker")
),
Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oF"),
libraryDependencies ++= Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private[execution] class AsyncResolverBuilder[F[_]: Async](asyncToFuture: AsyncT
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down Expand Up @@ -77,7 +77,7 @@ private[execution] class AsyncResolver[Ctx, F[_]: Async](
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
package sangria.execution

import sangria.schema.{Context, EnumType}
import sangria.schema._

trait DeprecationTracker {
def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]): Unit
def deprecatedEnumValueUsed[T, Ctx](`enum`: EnumType[T], value: T, userContext: Ctx): Unit
def deprecatedDirectiveArgUsed[Ctx](
directive: Directive,
argument: Argument[_],
ctx: Context[Ctx, _]): Unit
def deprecatedInputObjectFieldUsed[T, Ctx](
inputObject: InputObjectType[T],
field: InputField[_],
ctx: Context[Ctx, _]): Unit
def deprecatedFieldArgUsed[Ctx](
argument: Argument[_],
ctx: Context[Ctx, _]
): Unit
}

object DeprecationTracker {
val empty = NilDeprecationTracker
val print = new LoggingDeprecationTracker(println)
}

object NilDeprecationTracker extends DeprecationTracker {
def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]) = ()
def deprecatedEnumValueUsed[T, Ctx](`enum`: EnumType[T], value: T, userContext: Ctx) = ()
}

class LoggingDeprecationTracker(logFn: String => Unit) extends DeprecationTracker {
def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]) =
logFn(
s"Deprecated field '${ctx.parentType.name}.${ctx.field.name}' used at path '${ctx.path}'.")

def deprecatedEnumValueUsed[T, Ctx](`enum`: EnumType[T], value: T, userContext: Ctx) =
logFn(s"Deprecated enum value '$value' used of enum '${`enum`.name}'.")

def deprecatedDirectiveArgUsed[Ctx](
directive: Directive,
argument: Argument[_],
ctx: Context[Ctx, _]) =
logFn(s"Deprecated argument '${argument.name}' used of directive '${directive.name}'.")

def deprecatedInputObjectFieldUsed[T, Ctx](
inputObject: InputObjectType[T],
field: InputField[_],
ctx: Context[Ctx, _]) = logFn(
s"Deprecated field '${field.name}' used of input object '${inputObject.name}'.")

def deprecatedFieldArgUsed[Ctx](
argument: Argument[_],
ctx: Context[Ctx, _]
) = logFn(
s"Deprecated argument '${argument.name}' used of field '${ctx.parentType.name}'.${ctx.field.name}'.")
}
6 changes: 3 additions & 3 deletions modules/core/src/main/scala/sangria/execution/Executor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ case class Executor[Ctx, Root](
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down Expand Up @@ -326,7 +326,7 @@ object Executor {
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down Expand Up @@ -358,7 +358,7 @@ object Executor {
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down
130 changes: 125 additions & 5 deletions modules/core/src/main/scala/sangria/execution/FutureResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import sangria.schema._
import sangria.streaming.SubscriptionStream

import scala.annotation.tailrec
import scala.collection.immutable.VectorBuilder
import scala.collection.immutable.{ListMap, VectorBuilder}
import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.control.NonFatal
import scala.util.{Failure, Success}
Expand All @@ -25,7 +26,7 @@ private[execution] object FutureResolverBuilder extends ResolverBuilder {
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down Expand Up @@ -69,7 +70,7 @@ private[execution] class FutureResolver[Ctx](
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down Expand Up @@ -1534,6 +1535,126 @@ private[execution] class FutureResolver[Ctx](
Result(errorReg, if (canceled) None else Some(marshaller.arrayNode(listBuilder.result())))
}

private def trackDeprecation(
deprecationTracker: DeprecationTracker,
ctx: Context[Ctx, _]): Unit = {
val fieldArgs = ctx.args
val visitedDirectives = mutable.Set[String]()

def getArgValue(name: String, args: Args): Option[_] =
if (args.argDefinedInQuery(name)) {
if (args.optionalArgs.contains(name)) {
args.argOpt(name)
} else {
Some(args.arg(name))
}
} else {
None
}

def deprecatedArgsUsed(argDefs: List[Argument[_]], argValues: Args): List[Argument[_]] =
argDefs.filter { argDef =>
val argValue = getArgValue(argDef.name, argValues)
argDef.deprecationReason.isDefined && argValue.isDefined
}

def trackDeprecatedDirectiveArgs(astDirective: ast.Directive): Unit = {
// prevent infinite loop from directiveA -> arg -> directiveA -> arg ...
Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure if this is something we can actually run into with query parsing, but it is something that was possible to run into with tests

if (visitedDirectives.contains(astDirective.name)) {
return
}
visitedDirectives.add(astDirective.name)

ctx.schema.directives.find(_.name == astDirective.name) match {
case Some(directive) =>
val directiveArgs = valueCollector
.getArgumentValues(
Some(astDirective),
directive.arguments,
astDirective.arguments,
variables)

directiveArgs match {
case Success(directiveArgs) =>
deprecatedArgsUsed(directive.arguments, directiveArgs).foreach { arg =>
deprecationTracker.deprecatedDirectiveArgUsed(directive, arg, ctx)
}
case _ => // if we fail to get args, the query should fail elsewhere
}

// nested argument directives
directive.arguments.foreach { nestedArg =>
nestedArg.astDirectives.foreach(trackDeprecatedDirectiveArgs)
}
case _ => // do nothing
}
}

def trackDeprecatedInputObjectFields(inputType: InputType[_], ioArg: Any): Unit =
inputType match {
case ioDef: InputObjectType[_] =>
ioDef.fields.foreach { field =>
// field deprecation
val fieldVal: Option[_] = (ioArg match {
case lm: ListMap[String, _] => lm.get(field.name)
case _ => None
}) match {
case Some(Some(nested)) => Some(nested)
case value => value
}

if (field.deprecationReason.isDefined && fieldVal.isDefined) {
deprecationTracker.deprecatedInputObjectFieldUsed(ioDef, field, ctx)
}

// for nested input objects
if (fieldVal.isDefined) trackDeprecatedInputObjectFields(field.fieldType, fieldVal.get)

// field directive args deprecation
field.astDirectives.foreach(trackDeprecatedDirectiveArgs)
}
case OptionInputType(ofType) =>
ioArg match {
case Some(ioArg) => trackDeprecatedInputObjectFields(ofType, ioArg)
case _ => trackDeprecatedInputObjectFields(ofType, ioArg)
}
case ListInputType(ofType) =>
ioArg match {
case seq: Seq[_] => seq.foreach(trackDeprecatedInputObjectFields(ofType, _))
case _ => // do nothing
}
case _ => // do nothing
}

val field = ctx.field
val astField = ctx.astFields.head

// field deprecation
val allFields =
ctx.parentType.getField(ctx.schema, astField.name).asInstanceOf[Vector[Field[Ctx, Any]]]
if (allFields.exists(_.deprecationReason.isDefined))
deprecationTracker.deprecatedFieldUsed(ctx)

// directive argument deprecation
field.astDirectives.foreach(trackDeprecatedDirectiveArgs)

// field argument deprecation
deprecatedArgsUsed(field.arguments, fieldArgs).foreach { arg =>
deprecationTracker.deprecatedFieldArgUsed(arg, ctx)
}

field.arguments.foreach { argDef =>
// argument directives args deprecation
argDef.astDirectives.foreach(trackDeprecatedDirectiveArgs)

// input object field deprecation
getArgValue(argDef.name, fieldArgs) match {
case Some(ioArg) => trackDeprecatedInputObjectFields(argDef.argumentType, ioArg)
case _ => // do nothing
}
}
}

private def resolveField(
userCtx: Ctx,
tpe: ObjectType[Ctx, _],
Expand Down Expand Up @@ -1572,8 +1693,7 @@ private[execution] class FutureResolver[Ctx](
deferredResolverState = deferredResolverState
)

if (allFields.exists(_.deprecationReason.isDefined))
deprecationTracker.deprecatedFieldUsed(ctx)
deprecationTracker.foreach(trackDeprecation(_, ctx))

try {
val mBefore = middleware.collect { case (mv, m: MiddlewareBeforeField[Ctx]) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ case class InputDocumentMaterializer[Vars](
schema,
variables,
document.sourceMapper,
DeprecationTracker.empty,
None,
(),
ExceptionHandler.empty,
None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object QueryReducerExecutor {
operationName: Option[String] = None,
queryValidator: QueryValidator = QueryValidator.default,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
errorsLimit: Option[Int] = None
)(implicit executionContext: ExecutionContext): Future[(Ctx, TimeMeasurement)] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private[execution] trait ResolverBuilder {
exceptionHandler: ExceptionHandler,
deferredResolver: DeferredResolver[Ctx],
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
middleware: List[(Any, Middleware[Ctx])],
maxQueryDepth: Option[Int],
deferredResolverState: Any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import scala.collection.immutable.VectorBuilder

class ValueCoercionHelper[Ctx](
sourceMapper: Option[SourceMapper] = None,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
userContext: Option[Ctx] = None) {
import ValueCoercionHelper.defaultValueMapFn

Expand Down Expand Up @@ -527,7 +527,7 @@ class ValueCoercionHelper[Ctx](
isArgument))),
{ case (v, deprecated) =>
if (deprecated && userContext.isDefined)
deprecationTracker.deprecatedEnumValueUsed(enumT, v, userContext.get)
deprecationTracker.foreach(_.deprecatedEnumValueUsed(enumT, v, userContext.get))

val prepared = firstKindMarshaller match {
case raw: RawResultMarshaller => raw.rawScalarNode(v)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ValueCollector[Ctx, Input](
schema: Schema[_, _],
inputVars: Input,
sourceMapper: Option[SourceMapper],
deprecationTracker: DeprecationTracker,
deprecationTracker: Option[DeprecationTracker],
userContext: Ctx,
exceptionHandler: ExceptionHandler,
fromScalarMiddleware: Option[(Any, InputType[_]) => Option[Either[Violation, Any]]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ object BatchExecutor {
queryValidator: QueryValidator = QueryValidator.default,
deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty,
exceptionHandler: ExceptionHandler = ExceptionHandler.empty,
deprecationTracker: DeprecationTracker = DeprecationTracker.empty,
deprecationTracker: Option[DeprecationTracker] = None,
middleware: List[Middleware[Ctx]] = Nil,
maxQueryDepth: Option[Int] = None,
queryReducers: List[QueryReducer[Ctx, _]] = Nil,
Expand Down
Loading