mirror of
https://github.com/fluencelabs/aqua.git
synced 2024-12-04 14:40:17 +00:00
fix(compiler): Type check arrow calls on services and abilities [LNG-315] (#1037)
* Rewrite resolveIntoArrow * Refactor * Refactor resolveIntoCopy * Refactor resolveIntoIndex * Refactor resolveIntoField * Fix test * Remove package-lock.json * Add tests * Add comment
This commit is contained in:
parent
5241f522d8
commit
d46ee0347f
@ -7,7 +7,7 @@ ability WorkerJob:
|
||||
|
||||
func disjoint_run{WorkerJob}() -> -> string:
|
||||
run = func () -> string:
|
||||
r <- WorkerJob.runOnSingleWorker()
|
||||
r <- WorkerJob.runOnSingleWorker("worker")
|
||||
<- r
|
||||
<- run
|
||||
|
||||
|
@ -53,29 +53,38 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
|
||||
private def resolveSingleProperty(rootType: Type, op: PropertyOp[S]): Alg[Option[PropertyRaw]] =
|
||||
op match {
|
||||
case op: IntoField[S] =>
|
||||
T.resolveField(rootType, op)
|
||||
case op: IntoArrow[S] =>
|
||||
for {
|
||||
maybeArgs <- op.arguments.traverse(valueToRaw)
|
||||
arrowProp <- maybeArgs.sequence.flatTraverse(
|
||||
T.resolveArrow(rootType, op, _)
|
||||
OptionT(T.resolveIntoField(op, rootType))
|
||||
.map(
|
||||
_.fold(
|
||||
field = t => IntoFieldRaw(op.value, t),
|
||||
property = t => FunctorRaw(op.value, t)
|
||||
)
|
||||
)
|
||||
} yield arrowProp
|
||||
.value
|
||||
case op: IntoArrow[S] =>
|
||||
(for {
|
||||
args <- op.arguments.traverse(arg => OptionT(valueToRaw(arg)))
|
||||
argTypes = args.map(_.`type`)
|
||||
arrowType <- OptionT(T.resolveIntoArrow(op, rootType, argTypes))
|
||||
} yield IntoArrowRaw(op.name.value, arrowType, args)).value
|
||||
case op: IntoCopy[S] =>
|
||||
(for {
|
||||
_ <- OptionT.liftF(
|
||||
reportNamedArgsDuplicates(op.args)
|
||||
)
|
||||
fields <- op.args.traverse(arg => OptionT(valueToRaw(arg.argValue)).map(arg -> _))
|
||||
prop <- OptionT(T.resolveCopy(op, rootType, fields))
|
||||
} yield prop).value
|
||||
case op: IntoIndex[S] =>
|
||||
for {
|
||||
maybeIdx <- op.idx.fold(LiteralRaw.Zero.some.pure)(valueToRaw)
|
||||
idxProp <- maybeIdx.flatTraverse(
|
||||
T.resolveIndex(rootType, op, _)
|
||||
args <- op.args.traverse(arg =>
|
||||
OptionT(valueToRaw(arg.argValue)).map(
|
||||
arg.argName.value -> _
|
||||
)
|
||||
)
|
||||
} yield idxProp
|
||||
argsTypes = args.map { case (_, raw) => raw.`type` }
|
||||
structType <- OptionT(T.resolveIntoCopy(op, rootType, argsTypes))
|
||||
} yield IntoCopyRaw(structType, args.toNem)).value
|
||||
case op: IntoIndex[S] =>
|
||||
(for {
|
||||
idx <- OptionT(op.idx.fold(LiteralRaw.Zero.some.pure)(valueToRaw))
|
||||
valueType <- OptionT(T.resolveIntoIndex(op, rootType, idx.`type`))
|
||||
} yield IntoIndexRaw(idx, valueType)).value
|
||||
}
|
||||
|
||||
def valueToRaw(v: ValueToken[S]): Alg[Option[ValueRaw]] =
|
||||
|
@ -40,21 +40,74 @@ trait TypesAlgebra[S[_], Alg[_]] {
|
||||
|
||||
def defineAlias(name: NamedTypeToken[S], target: Type): Alg[Boolean]
|
||||
|
||||
def resolveIndex(rootT: Type, op: IntoIndex[S], idx: ValueRaw): Alg[Option[PropertyRaw]]
|
||||
|
||||
def resolveCopy(
|
||||
token: IntoCopy[S],
|
||||
/**
|
||||
* Resolve `IntoIndex` property on value with `rootT` type
|
||||
*
|
||||
* @param op property to resolve
|
||||
* @param rootT type of the value to which property is applied
|
||||
* @param idxType type of the index
|
||||
* @return type of the value at given index if property application is valid
|
||||
*/
|
||||
def resolveIntoIndex(
|
||||
op: IntoIndex[S],
|
||||
rootT: Type,
|
||||
fields: NonEmptyList[(NamedArg[S], ValueRaw)]
|
||||
): Alg[Option[PropertyRaw]]
|
||||
idxType: Type
|
||||
): Alg[Option[DataType]]
|
||||
|
||||
def resolveField(rootT: Type, op: IntoField[S]): Alg[Option[PropertyRaw]]
|
||||
|
||||
def resolveArrow(
|
||||
/**
|
||||
* Resolve `IntoCopy` property on value with `rootT` type
|
||||
*
|
||||
* @param op property to resolve
|
||||
* @param rootT type of the value to which property is applied
|
||||
* @param types types of arguments passed
|
||||
* @return struct type if property application is valid
|
||||
* @note `types` should correspond to `op.args`
|
||||
*/
|
||||
def resolveIntoCopy(
|
||||
op: IntoCopy[S],
|
||||
rootT: Type,
|
||||
types: NonEmptyList[Type]
|
||||
): Alg[Option[StructType]]
|
||||
|
||||
enum IntoFieldRes(`type`: Type) {
|
||||
case Field(`type`: Type) extends IntoFieldRes(`type`)
|
||||
case Property(`type`: Type) extends IntoFieldRes(`type`)
|
||||
|
||||
def fold[A](field: Type => A, property: Type => A): A =
|
||||
this match {
|
||||
case Field(t) => field(t)
|
||||
case Property(t) => property(t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `IntoField` property on value with `rootT` type
|
||||
*
|
||||
* @param op property to resolve
|
||||
* @param rootT type of the value to which property is applied
|
||||
* @return if property application is valid, return
|
||||
* Field(type) if it's a field of rootT (fields of structs or abilities),
|
||||
* Property(type) if it's a property of rootT (functors of collections)
|
||||
*/
|
||||
def resolveIntoField(
|
||||
op: IntoField[S],
|
||||
rootT: Type
|
||||
): Alg[Option[IntoFieldRes]]
|
||||
|
||||
/**
|
||||
* Resolve `IntoArrow` property on value with `rootT` type
|
||||
*
|
||||
* @param op property to resolve
|
||||
* @param rootT type of the value to which property is applied
|
||||
* @param types types of arguments passed
|
||||
* @return arrow type if property application is valid
|
||||
* @note `types` should correspond to `op.arguments`
|
||||
*/
|
||||
def resolveIntoArrow(
|
||||
op: IntoArrow[S],
|
||||
arguments: List[ValueRaw]
|
||||
): Alg[Option[PropertyRaw]]
|
||||
rootT: Type,
|
||||
types: List[Type]
|
||||
): Alg[Option[ArrowType]]
|
||||
|
||||
def ensureValuesComparable(token: Token[S], left: Type, right: Type): Alg[Boolean]
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package aqua.semantics.rules.types
|
||||
|
||||
import aqua.errors.Errors.internalError
|
||||
import aqua.parser.lexer.*
|
||||
import aqua.raw.value.*
|
||||
import aqua.semantics.Levenshtein
|
||||
@ -17,6 +18,7 @@ import cats.syntax.apply.*
|
||||
import cats.syntax.flatMap.*
|
||||
import cats.syntax.foldable.*
|
||||
import cats.syntax.functor.*
|
||||
import cats.syntax.monad.*
|
||||
import cats.syntax.option.*
|
||||
import cats.syntax.traverse.*
|
||||
import cats.{Applicative, ~>}
|
||||
@ -187,132 +189,177 @@ class TypesInterpreter[S[_], X](using
|
||||
.as(true)
|
||||
}
|
||||
|
||||
override def resolveField(rootT: Type, op: IntoField[S]): State[X, Option[PropertyRaw]] = {
|
||||
override def resolveIntoField(
|
||||
op: IntoField[S],
|
||||
rootT: Type
|
||||
): State[X, Option[IntoFieldRes]] = {
|
||||
rootT match {
|
||||
case nt: NamedType =>
|
||||
nt.fields(op.value)
|
||||
.fold(
|
||||
nt.fields(op.value) match {
|
||||
case Some(t) =>
|
||||
locations
|
||||
.pointFieldLocation(nt.name, op.value, op)
|
||||
.as(Some(IntoFieldRes.Field(t)))
|
||||
case None =>
|
||||
val fields = nt.fields.keys.map(k => s"`$k`").toList.mkString(", ")
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Field `${op.value}` not found in type `${nt.name}`, available: ${nt.fields.toNel.toList.map(_._1).mkString(", ")}"
|
||||
s"Field `${op.value}` not found in type `${nt.name}`, available: $fields"
|
||||
)
|
||||
.as(None)
|
||||
) { t =>
|
||||
locations.pointFieldLocation(nt.name, op.value, op).as(Some(IntoFieldRaw(op.value, t)))
|
||||
}
|
||||
}
|
||||
case t =>
|
||||
t.properties
|
||||
.get(op.value)
|
||||
.fold(
|
||||
.get(op.value) match {
|
||||
case Some(t) =>
|
||||
State.pure(Some(IntoFieldRes.Property(t)))
|
||||
case None =>
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Expected data type to resolve a field '${op.value}' or a type with this property. Got: $rootT"
|
||||
s"Property `${op.value}` not found in type `$t`"
|
||||
)
|
||||
.as(None)
|
||||
)(t => State.pure(Some(FunctorRaw(op.value, t))))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def resolveArrow(
|
||||
rootT: Type,
|
||||
override def resolveIntoArrow(
|
||||
op: IntoArrow[S],
|
||||
arguments: List[ValueRaw]
|
||||
): State[X, Option[PropertyRaw]] = {
|
||||
rootT: Type,
|
||||
types: List[Type]
|
||||
): State[X, Option[ArrowType]] = {
|
||||
/* Safeguard to check condition on arguments */
|
||||
if (op.arguments.length != types.length)
|
||||
internalError(s"Invalid arguments, lists do not match: ${op.arguments} and $types")
|
||||
|
||||
val opName = op.name.value
|
||||
|
||||
rootT match {
|
||||
case ab: GeneralAbilityType =>
|
||||
val name = ab.name
|
||||
val fields = ab.fields
|
||||
lazy val fieldNames = fields.toNel.toList.map(_._1).mkString(", ")
|
||||
fields(op.name.value)
|
||||
.fold(
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Arrow `${op.name.value}` not found in type `$name`, " +
|
||||
s"available: $fieldNames"
|
||||
)
|
||||
.as(None)
|
||||
) {
|
||||
case at @ ArrowType(_, _) =>
|
||||
locations
|
||||
.pointFieldLocation(name, op.name.value, op)
|
||||
.as(Some(IntoArrowRaw(op.name.value, at, arguments)))
|
||||
case _ =>
|
||||
val abName = ab.fullName
|
||||
|
||||
ab.fields.lookup(opName) match {
|
||||
case Some(at: ArrowType) =>
|
||||
val reportNotEnoughArguments =
|
||||
/* Report at position of arrow application */
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Unexpected. `${op.name.value}` must be an arrow."
|
||||
s"Not enough arguments for arrow `$opName` in `$abName`, " +
|
||||
s"expected: ${at.domain.length}, given: ${op.arguments.length}"
|
||||
)
|
||||
.as(None)
|
||||
}
|
||||
case t =>
|
||||
t.properties
|
||||
.get(op.name.value)
|
||||
.fold(
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Expected type to resolve an arrow '${op.name.value}' or a type with this property. Got: $rootT"
|
||||
)
|
||||
.as(None)
|
||||
)(t => State.pure(Some(FunctorRaw(op.name.value, t))))
|
||||
.whenA(op.arguments.length < at.domain.length)
|
||||
val reportTooManyArguments =
|
||||
/* Report once at position of the first extra argument */
|
||||
op.arguments.drop(at.domain.length).headOption.traverse_ { arg =>
|
||||
report
|
||||
.error(
|
||||
arg,
|
||||
s"Too many arguments for arrow `$opName` in `$abName`, " +
|
||||
s"expected: ${at.domain.length}, given: ${op.arguments.length}"
|
||||
)
|
||||
}
|
||||
val checkArgumentTypes =
|
||||
op.arguments
|
||||
.zip(types)
|
||||
.zip(at.domain.toList)
|
||||
.forallM { case ((arg, argType), expectedType) =>
|
||||
ensureTypeMatches(arg, expectedType, argType)
|
||||
}
|
||||
|
||||
locations.pointFieldLocation(abName, opName, op) *>
|
||||
reportNotEnoughArguments *>
|
||||
reportTooManyArguments *>
|
||||
checkArgumentTypes.map(typesMatch =>
|
||||
Option.when(
|
||||
typesMatch && at.domain.length == op.arguments.length
|
||||
)(at)
|
||||
)
|
||||
|
||||
case Some(t) =>
|
||||
report
|
||||
.error(op, s"Field `$opName` has non arrow type `$t` in `$abName`")
|
||||
.as(None)
|
||||
case None =>
|
||||
val available = ab.arrowFields.keys.map(k => s"`$k`").mkString(", ")
|
||||
report
|
||||
.error(op, s"Arrow `$opName` not found in `$abName`, available: $available")
|
||||
.as(None)
|
||||
}
|
||||
case t =>
|
||||
/* NOTE: Arrows are only supported on services and abilities,
|
||||
(`.copy(...)` for structs is resolved by separate method) */
|
||||
report
|
||||
.error(op, s"Arrow `$opName` not found in `$t`")
|
||||
.as(None)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO actually it's stateless, exists there just for reporting needs
|
||||
override def resolveCopy(
|
||||
token: IntoCopy[S],
|
||||
override def resolveIntoCopy(
|
||||
op: IntoCopy[S],
|
||||
rootT: Type,
|
||||
args: NonEmptyList[(NamedArg[S], ValueRaw)]
|
||||
): State[X, Option[PropertyRaw]] =
|
||||
types: NonEmptyList[Type]
|
||||
): State[X, Option[StructType]] = {
|
||||
if (op.args.length != types.length)
|
||||
internalError(s"Invalid arguments, lists do not match: ${op.args} and $types")
|
||||
|
||||
rootT match {
|
||||
case st: StructType =>
|
||||
args.forallM { case (arg, value) =>
|
||||
val fieldName = arg.argName.value
|
||||
st.fields.lookup(fieldName) match {
|
||||
case Some(t) =>
|
||||
ensureTypeMatches(arg.argValue, t, value.`type`)
|
||||
case None =>
|
||||
report.error(arg.argName, s"No field with name '$fieldName' in $rootT").as(false)
|
||||
op.args
|
||||
.zip(types)
|
||||
.forallM { case (arg, argType) =>
|
||||
val fieldName = arg.argName.value
|
||||
st.fields.lookup(fieldName) match {
|
||||
case Some(fieldType) =>
|
||||
ensureTypeMatches(arg.argValue, fieldType, argType)
|
||||
case None =>
|
||||
report
|
||||
.error(
|
||||
arg.argName,
|
||||
s"No field with name '$fieldName' in `$st`"
|
||||
)
|
||||
.as(false)
|
||||
}
|
||||
}
|
||||
}.map(
|
||||
Option.when(_)(
|
||||
IntoCopyRaw(
|
||||
st,
|
||||
args.map { case (arg, value) =>
|
||||
arg.argName.value -> value
|
||||
}.toNem
|
||||
)
|
||||
.map(Option.when(_)(st))
|
||||
case t =>
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Non data type `$t` does not support `.copy`"
|
||||
)
|
||||
)
|
||||
|
||||
case _ =>
|
||||
report.error(token, s"Expected $rootT to be a data type").as(None)
|
||||
.as(None)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO actually it's stateless, exists there just for reporting needs
|
||||
override def resolveIndex(
|
||||
rootT: Type,
|
||||
override def resolveIntoIndex(
|
||||
op: IntoIndex[S],
|
||||
idx: ValueRaw
|
||||
): State[X, Option[PropertyRaw]] =
|
||||
if (!ScalarType.i64.acceptsValueOf(idx.`type`))
|
||||
report.error(op, s"Expected numeric index, got $idx").as(None)
|
||||
else
|
||||
rootT match {
|
||||
case ot: OptionType =>
|
||||
op.idx.fold(
|
||||
State.pure(Some(IntoIndexRaw(idx, ot.element)))
|
||||
)(v => report.error(v, s"Options might have only one element, use ! to get it").as(None))
|
||||
case rt: CollectionType =>
|
||||
State.pure(Some(IntoIndexRaw(idx, rt.element)))
|
||||
case _ =>
|
||||
report.error(op, s"Expected $rootT to be a collection type").as(None)
|
||||
}
|
||||
rootT: Type,
|
||||
idxType: Type
|
||||
): State[X, Option[DataType]] =
|
||||
ensureTypeOneOf(
|
||||
op.idx.getOrElse(op),
|
||||
ScalarType.integer,
|
||||
idxType
|
||||
) *> (rootT match {
|
||||
case ot: OptionType =>
|
||||
op.idx.fold(State.pure(Some(ot.element)))(v =>
|
||||
// TODO: Is this a right place to report this error?
|
||||
// It is not a type error, but rather a syntax error
|
||||
report.error(v, s"Options might have only one element, use ! to get it").as(None)
|
||||
)
|
||||
case rt: CollectionType =>
|
||||
State.pure(Some(rt.element))
|
||||
case t =>
|
||||
report
|
||||
.error(
|
||||
op,
|
||||
s"Non collection type `$t` does not support indexing"
|
||||
)
|
||||
.as(None)
|
||||
})
|
||||
|
||||
override def ensureValuesComparable(
|
||||
token: Token[S],
|
||||
@ -423,7 +470,7 @@ class TypesInterpreter[S[_], X](using
|
||||
): State[X, Boolean] = for {
|
||||
/* Check that required fields are present
|
||||
among arguments and have correct types */
|
||||
enough <- expected.fields.toNel.traverse { case (name, typ) =>
|
||||
enough <- expected.fields.toNel.forallM { case (name, typ) =>
|
||||
arguments.lookup(name) match {
|
||||
case Some(arg -> givenType) =>
|
||||
ensureTypeMatches(arg.argValue, typ, givenType)
|
||||
@ -435,7 +482,7 @@ class TypesInterpreter[S[_], X](using
|
||||
)
|
||||
.as(false)
|
||||
}
|
||||
}.map(_.forall(identity))
|
||||
}
|
||||
expectedKeys = expected.fields.keys.toNonEmptyList
|
||||
/* Report unexpected arguments */
|
||||
_ <- arguments.toNel.traverse_ { case (name, arg -> typ) =>
|
||||
|
@ -65,12 +65,30 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside {
|
||||
def stream(values: ValueToken[Id]*): CollectionToken[Id] =
|
||||
CollectionToken[Id](CollectionToken.Mode.StreamMode, values.toList)
|
||||
|
||||
def serviceCall(
|
||||
srv: String,
|
||||
method: String,
|
||||
args: List[ValueToken[Id]] = Nil
|
||||
): PropertyToken[Id] =
|
||||
PropertyToken(
|
||||
variable(srv),
|
||||
NonEmptyList.of(
|
||||
IntoArrow(
|
||||
Name[Id](method),
|
||||
args
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def allPairs[A](list: List[A]): List[(A, A)] = for {
|
||||
a <- list
|
||||
b <- list
|
||||
} yield (a, b)
|
||||
|
||||
def genState(vars: Map[String, Type] = Map.empty) = {
|
||||
def genState(
|
||||
vars: Map[String, Type] = Map.empty,
|
||||
types: Map[String, Type] = Map.empty
|
||||
) = {
|
||||
val init = RawContext.blank.copy(
|
||||
parts = Chain
|
||||
.fromSeq(ConstantRaw.defaultConstants())
|
||||
@ -88,6 +106,10 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside {
|
||||
) :: _
|
||||
)
|
||||
)
|
||||
.focus(_.types)
|
||||
.modify(types.foldLeft(_) { case (st, (name, t)) =>
|
||||
st.defineType(NamedTypeToken(name), t)
|
||||
})
|
||||
}
|
||||
|
||||
def valueOfType(t: Type)(
|
||||
@ -626,4 +648,72 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside {
|
||||
value.`type` shouldBe OptionType(BottomType)
|
||||
}
|
||||
}
|
||||
|
||||
it should "type check service calls" in {
|
||||
val srvName = "TestSrv"
|
||||
val methodName = "testMethod"
|
||||
val methodType = ArrowType(
|
||||
ProductType(ScalarType.i8 :: ScalarType.string :: Nil),
|
||||
ProductType(Nil)
|
||||
)
|
||||
|
||||
def test(args: List[ValueToken[Id]], vars: Map[String, Type] = Map.empty) = {
|
||||
val state = genState(
|
||||
vars,
|
||||
types = Map(
|
||||
srvName -> ServiceType(
|
||||
srvName,
|
||||
NonEmptyMap.of(
|
||||
methodName -> methodType
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val call = serviceCall(srvName, methodName, args)
|
||||
|
||||
val alg = algebra()
|
||||
val (st, res) = alg
|
||||
.valueToRaw(call)
|
||||
.run(state)
|
||||
.value
|
||||
|
||||
res shouldBe None
|
||||
atLeast(1, st.errors.toList) shouldBe a[RulesViolated[Id]]
|
||||
}
|
||||
|
||||
// not enough arguments
|
||||
// TestSrv.testMethod()
|
||||
test(List.empty)
|
||||
// TestSrv.testMethod(42)
|
||||
test(literal("42", LiteralType.unsigned) :: Nil)
|
||||
// TestSrv.testMethod(var)
|
||||
test(variable("var") :: Nil, Map("var" -> ScalarType.i8))
|
||||
|
||||
// wrong argument type
|
||||
// TestSrv.testMethod([42, var])
|
||||
test(
|
||||
array(literal("42", LiteralType.unsigned), variable("var")) :: Nil,
|
||||
Map("var" -> ScalarType.i8)
|
||||
)
|
||||
// TestSrv.testMethod(42, var)
|
||||
test(
|
||||
literal("42", LiteralType.unsigned) :: variable("var") :: Nil,
|
||||
Map("var" -> ScalarType.i64)
|
||||
)
|
||||
// TestSrv.testMethod("test", var)
|
||||
test(
|
||||
literal("test", LiteralType.string) :: variable("var") :: Nil,
|
||||
Map("var" -> ScalarType.string)
|
||||
)
|
||||
|
||||
// too many arguments
|
||||
// TestSrv.testMethod(42, "test", var)
|
||||
test(
|
||||
literal("42", LiteralType.unsigned) ::
|
||||
literal("test", LiteralType.string) ::
|
||||
variable("var") :: Nil,
|
||||
Map("var" -> ScalarType.string)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -370,6 +370,11 @@ sealed trait NamedType extends Type {
|
||||
|
||||
def fields: NonEmptyMap[String, Type]
|
||||
|
||||
def arrowFields: Map[String, ArrowType] =
|
||||
fields.toSortedMap.collect { case (name, at: ArrowType) =>
|
||||
name -> at
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fields defined in this type and its fields of named type.
|
||||
* Paths to fields are returned **without** type name
|
||||
|
Loading…
Reference in New Issue
Block a user