diff --git a/Sources/GraphQL/Error/GraphQLError.swift b/Sources/GraphQL/Error/GraphQLError.swift index b1a93353..fd8876a3 100644 --- a/Sources/GraphQL/Error/GraphQLError.swift +++ b/Sources/GraphQL/Error/GraphQLError.swift @@ -197,7 +197,7 @@ extension IndexPath: ExpressibleByArrayLiteral { } } -public enum IndexPathValue: Codable { +public enum IndexPathValue: Codable, Equatable { case index(Int) case key(String) @@ -242,7 +242,7 @@ extension IndexPathValue: CustomStringConvertible { } } -public protocol IndexPathElement { +public protocol IndexPathElement: CustomStringConvertible { var indexPathValue: IndexPathValue { get } } diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 44c285c0..3301e621 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -2,7 +2,7 @@ * Contains a range of UTF-8 character offsets and token references that * identify the region of the source from which the AST derived. */ -public struct Location { +public struct Location: Equatable { /** * The character offset at which this Node begins. */ @@ -159,6 +159,43 @@ public enum NodeResult { } return false } + + func get(key: IndexPathElement) -> NodeResult? { + switch self { + case let .node(node): + guard let key = key.keyValue else { + return nil + } + return node.get(key: key) + case let .array(array): + guard let key = key.indexValue else { + return nil + } + return .node(array[key]) + } + } + + func set(value: NodeResult, key: IndexPathElement) -> Self? { + switch self { + case let .node(node): + guard let key = key.keyValue else { + return nil + } + node.set(value: value, key: key) + return .node(node) + case var .array(array): + switch value { + case let .node(value): + guard let key = key.indexValue else { + return nil + } + array[key] = value + return .array(array) + case let .array(value): + return .array(value) + } + } + } } /** @@ -168,7 +205,7 @@ public protocol Node { var kind: Kind { get } var loc: Location? { get } func get(key: String) -> NodeResult? - func set(value: Node?, key: String) + func set(value: NodeResult?, key: String) } public extension Node { @@ -176,7 +213,14 @@ public extension Node { return nil } - func set(value _: Node?, key _: String) {} + @available(*, deprecated, message: "Use set(value _: NodeResult?, key _: String)") + func set(value: Node?, key: String) { + return set(value: value.map { .node($0) }, key: key) + } + + func set(value _: NodeResult?, key _: String) { + // This should be overridden by each type on which it should do something + } } extension Name: Node {} @@ -237,7 +281,7 @@ extension Name: Equatable { public final class Document { public let kind: Kind = .document public let loc: Location? - public let definitions: [Definition] + public private(set) var definitions: [Definition] init(loc: Location? = nil, definitions: [Definition]) { self.loc = loc @@ -255,6 +299,24 @@ public final class Document { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "definitions": + guard + case let .array(values) = value, + let definitions = values as? [Definition] + else { + return + } + self.definitions = definitions + default: + return + } + } } extension Document: Equatable { @@ -308,10 +370,10 @@ public final class OperationDefinition { public let kind: Kind = .operationDefinition public let loc: Location? public let operation: OperationType - public let name: Name? - public let variableDefinitions: [VariableDefinition] - public let directives: [Directive] - public let selectionSet: SelectionSet + public private(set) var name: Name? + public private(set) var variableDefinitions: [VariableDefinition] + public private(set) var directives: [Directive] + public private(set) var selectionSet: SelectionSet init( loc: Location? = nil, @@ -349,6 +411,48 @@ public final class OperationDefinition { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "variableDefinitions": + guard + case let .array(values) = value, + let variableDefinitions = values as? [VariableDefinition] + else { + return + } + self.variableDefinitions = variableDefinitions + case "directives": + guard + case let .array(values) = value, + let directives = values as? [Directive] + else { + return + } + self.directives = directives + case "selectionSet": + guard + case let .node(value) = value, + let selectionSet = value as? SelectionSet + else { + return + } + self.selectionSet = selectionSet + default: + return + } + } } extension OperationDefinition: Hashable { @@ -368,9 +472,9 @@ extension OperationDefinition: Hashable { public final class VariableDefinition { public let kind: Kind = .variableDefinition public let loc: Location? - public let variable: Variable - public let type: Type - public let defaultValue: Value? + public private(set) var variable: Variable + public private(set) var type: Type + public private(set) var defaultValue: Value? init(loc: Location? = nil, variable: Variable, type: Type, defaultValue: Value? = nil) { self.loc = loc @@ -391,6 +495,40 @@ public final class VariableDefinition { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "variable": + guard + case let .node(node) = value, + let variable = node as? Variable + else { + return + } + self.variable = variable + case "type": + guard + case let .node(node) = value, + let type = node as? Type + else { + return + } + self.type = type + case "defaultValue": + guard + case let .node(node) = value, + let defaultValue = node as? Value? + else { + return + } + self.defaultValue = defaultValue + default: + return + } + } } extension VariableDefinition: Equatable { @@ -418,7 +556,7 @@ extension VariableDefinition: Equatable { public final class Variable { public let kind: Kind = .variable public let loc: Location? - public let name: Name + public private(set) var name: Name init(loc: Location? = nil, name: Name) { self.loc = loc @@ -433,6 +571,24 @@ public final class Variable { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + default: + return + } + } } extension Variable: Equatable { @@ -444,7 +600,7 @@ extension Variable: Equatable { public final class SelectionSet { public let kind: Kind = .selectionSet public let loc: Location? - public let selections: [Selection] + public private(set) var selections: [Selection] init(loc: Location? = nil, selections: [Selection]) { self.loc = loc @@ -462,6 +618,24 @@ public final class SelectionSet { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "selections": + guard + case let .array(values) = value, + let selections = values as? [Selection] + else { + return + } + self.selections = selections + default: + return + } + } } extension SelectionSet: Hashable { @@ -513,11 +687,11 @@ public func == (lhs: Selection, rhs: Selection) -> Bool { public final class Field { public let kind: Kind = .field public let loc: Location? - public let alias: Name? - public let name: Name - public let arguments: [Argument] - public let directives: [Directive] - public let selectionSet: SelectionSet? + public private(set) var alias: Name? + public private(set) var name: Name + public private(set) var arguments: [Argument] + public private(set) var directives: [Directive] + public private(set) var selectionSet: SelectionSet? init( loc: Location? = nil, @@ -557,6 +731,56 @@ public final class Field { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "alias": + guard + case let .node(node) = value, + let alias = node as? Name + else { + return + } + self.alias = alias + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "arguments": + guard + case let .array(values) = value, + let arguments = values as? [Argument] + else { + return + } + self.arguments = arguments + case "directives": + guard + case let .array(values) = value, + let directives = values as? [Directive] + else { + return + } + self.directives = directives + case "selectionSet": + guard + case let .node(value) = value, + let selectionSet = value as? SelectionSet + else { + return + } + self.selectionSet = selectionSet + default: + return + } + } } extension Field: Equatable { @@ -572,8 +796,8 @@ extension Field: Equatable { public final class Argument { public let kind: Kind = .argument public let loc: Location? - public let name: Name - public let value: Value + public private(set) var name: Name + public private(set) var value: Value init(loc: Location? = nil, name: Name, value: Value) { self.loc = loc @@ -591,6 +815,32 @@ public final class Argument { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "value": + guard + case let .node(node) = value, + let value = node as? Value + else { + return + } + self.value = value + default: + return + } + } } extension Argument: Equatable { @@ -607,8 +857,8 @@ extension InlineFragment: Fragment {} public final class FragmentSpread { public let kind: Kind = .fragmentSpread public let loc: Location? - public let name: Name - public let directives: [Directive] + public private(set) var name: Name + public private(set) var directives: [Directive] init(loc: Location? = nil, name: Name, directives: [Directive] = []) { self.loc = loc @@ -629,6 +879,32 @@ public final class FragmentSpread { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "directives": + guard + case let .array(values) = value, + let directives = values as? [Directive] + else { + return + } + self.directives = directives + default: + return + } + } } extension FragmentSpread: Equatable { @@ -657,9 +933,9 @@ extension FragmentDefinition: HasTypeCondition { public final class InlineFragment { public let kind: Kind = .inlineFragment public let loc: Location? - public let typeCondition: NamedType? - public let directives: [Directive] - public let selectionSet: SelectionSet + public private(set) var typeCondition: NamedType? + public private(set) var directives: [Directive] + public private(set) var selectionSet: SelectionSet init( loc: Location? = nil, @@ -690,6 +966,40 @@ public extension InlineFragment { return nil } } + + func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "typeCondition": + guard + case let .node(node) = value, + let typeCondition = node as? NamedType + else { + return + } + self.typeCondition = typeCondition + case "directives": + guard + case let .array(values) = value, + let directives = values as? [Directive] + else { + return + } + self.directives = directives + case "selectionSet": + guard + case let .node(value) = value, + let selectionSet = value as? SelectionSet + else { + return + } + self.selectionSet = selectionSet + default: + return + } + } } extension InlineFragment: Equatable { @@ -703,10 +1013,10 @@ extension InlineFragment: Equatable { public final class FragmentDefinition { public let kind: Kind = .fragmentDefinition public let loc: Location? - public let name: Name - public let typeCondition: NamedType - public let directives: [Directive] - public let selectionSet: SelectionSet + public private(set) var name: Name + public private(set) var typeCondition: NamedType + public private(set) var directives: [Directive] + public private(set) var selectionSet: SelectionSet init( loc: Location? = nil, @@ -739,6 +1049,48 @@ public final class FragmentDefinition { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "typeCondition": + guard + case let .node(node) = value, + let typeCondition = node as? NamedType + else { + return + } + self.typeCondition = typeCondition + case "directives": + guard + case let .array(values) = value, + let directives = values as? [Directive] + else { + return + } + self.directives = directives + case "selectionSet": + guard + case let .node(value) = value, + let selectionSet = value as? SelectionSet + else { + return + } + self.selectionSet = selectionSet + default: + return + } + } } extension FragmentDefinition: Hashable { @@ -915,12 +1267,39 @@ extension EnumValue: Equatable { public final class ListValue { public let kind: Kind = .listValue public let loc: Location? - public let values: [Value] + public private(set) var values: [Value] init(loc: Location? = nil, values: [Value]) { self.loc = loc self.values = values } + + public func get(key: String) -> NodeResult? { + switch key { + case "values": + return .array(values) + default: + return nil + } + } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "values": + guard + case let .array(values) = value, + let values = values as? [Value] + else { + return + } + self.values = values + default: + return + } + } } extension ListValue: Equatable { @@ -942,7 +1321,7 @@ extension ListValue: Equatable { public final class ObjectValue { public let kind: Kind = .objectValue public let loc: Location? - public let fields: [ObjectField] + public private(set) var fields: [ObjectField] init(loc: Location? = nil, fields: [ObjectField]) { self.loc = loc @@ -957,6 +1336,24 @@ public final class ObjectValue { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "fields": + guard + case let .array(values) = value, + let fields = values as? [ObjectField] + else { + return + } + self.fields = fields + default: + return + } + } } extension ObjectValue: Equatable { @@ -968,8 +1365,8 @@ extension ObjectValue: Equatable { public final class ObjectField { public let kind: Kind = .objectField public let loc: Location? - public let name: Name - public let value: Value + public private(set) var name: Name + public private(set) var value: Value init(loc: Location? = nil, name: Name, value: Value) { self.loc = loc @@ -987,6 +1384,32 @@ public final class ObjectField { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "value": + guard + case let .node(node) = value, + let value = node as? Value + else { + return + } + self.value = value + default: + return + } + } } extension ObjectField: Equatable { @@ -999,14 +1422,51 @@ extension ObjectField: Equatable { public final class Directive { public let kind: Kind = .directive public let loc: Location? - public let name: Name - public let arguments: [Argument] + public private(set) var name: Name + public private(set) var arguments: [Argument] init(loc: Location? = nil, name: Name, arguments: [Argument] = []) { self.loc = loc self.name = name self.arguments = arguments } + + public func get(key: String) -> NodeResult? { + switch key { + case "name": + return .node(name) + case "arguments": + return .array(arguments) + default: + return nil + } + } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + case "arguments": + guard + case let .array(nodes) = value, + let arguments = nodes as? [Argument] + else { + return + } + self.arguments = arguments + default: + return + } + } } extension Directive: Equatable { @@ -1045,7 +1505,7 @@ public func == (lhs: Type, rhs: Type) -> Bool { public final class NamedType { public let kind: Kind = .namedType public let loc: Location? - public let name: Name + public private(set) var name: Name init(loc: Location? = nil, name: Name) { self.loc = loc @@ -1060,6 +1520,24 @@ public final class NamedType { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "name": + guard + case let .node(node) = value, + let name = node as? Name + else { + return + } + self.name = name + default: + return + } + } } extension NamedType: Equatable { @@ -1071,12 +1549,39 @@ extension NamedType: Equatable { public final class ListType { public let kind: Kind = .listType public let loc: Location? - public let type: Type + public private(set) var type: Type init(loc: Location? = nil, type: Type) { self.loc = loc self.type = type } + + public func get(key: String) -> NodeResult? { + switch key { + case "type": + return .node(type) + default: + return nil + } + } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "type": + guard + case let .node(node) = value, + let type = node as? Type + else { + return + } + self.type = type + default: + return + } + } } extension ListType: Equatable { @@ -1092,7 +1597,7 @@ extension NamedType: NonNullableType {} public final class NonNullType { public let kind: Kind = .nonNullType public let loc: Location? - public let type: NonNullableType + public private(set) var type: NonNullableType init(loc: Location? = nil, type: NonNullableType) { self.loc = loc @@ -1107,6 +1612,24 @@ public final class NonNullType { return nil } } + + public func set(value: NodeResult?, key: String) { + guard let value = value else { + return + } + switch key { + case "type": + guard + case let .node(node) = value, + let type = node as? NonNullableType + else { + return + } + self.type = type + default: + return + } + } } extension NonNullType: Equatable { diff --git a/Sources/GraphQL/Language/Kinds.swift b/Sources/GraphQL/Language/Kinds.swift index 4c4fd0f2..48ba7882 100644 --- a/Sources/GraphQL/Language/Kinds.swift +++ b/Sources/GraphQL/Language/Kinds.swift @@ -1,4 +1,4 @@ -public enum Kind: CaseIterable { +public enum Kind: String, CaseIterable { case name case document case operationDefinition diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 293fd113..27d7a604 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -87,73 +87,58 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node var inArray = false var keys: [IndexPathElement] = ["root"] var index: Int = -1 - var edits: [(key: IndexPathElement, node: Node)] = [] + var edits: [(key: IndexPathElement, node: Node?)] = [] + var node: NodeResult? = .node(root) + var key: IndexPathElement? var parent: NodeResult? var path: [IndexPathElement] = [] var ancestors: [NodeResult] = [] - var newRoot = root repeat { index += 1 let isLeaving = index == keys.count - var key: IndexPathElement? - var node: NodeResult? let isEdited = isLeaving && !edits.isEmpty - if !isLeaving { - key = parent != nil ? inArray ? index : keys[index] : nil - - if let parent = parent { - switch parent { - case let .node(parent): - node = parent.get(key: key!.keyValue!) - case let .array(parent): - node = .node(parent[key!.indexValue!]) - } - } else { - node = .node(newRoot) - } - - if node == nil { - continue - } - - if parent != nil { - path.append(key!) - } - } else { - key = ancestors.isEmpty ? nil : path.popLast() + if isLeaving { + key = ancestors.isEmpty ? nil : path.last node = parent parent = ancestors.popLast() if isEdited { -// if inArray { -// node = node.slice() -// } else { -// let clone = node -// node = clone -// } -// -// var editOffset = 0 -// -// for ii in 0.. Node edits = stack!.edits inArray = stack!.inArray stack = stack!.prev + } else if let parent = parent { + key = keys[index] + node = parent.get(key: key!) + if node == nil { + continue + } + path.append(key!) } - var result: VisitResult - - if case let .node(n) = node! { + var result: VisitResult = .break // placeholder + if case let .node(n) = node { if !isLeaving { result = visitor.enter( node: n, @@ -186,17 +177,16 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node if case .break = result { break - } - - if case .skip = result, !isLeaving { - _ = path.popLast() - continue - } else if case let .node(n) = result { - edits.append((key!, n!)) - + } else if case .skip = result { if !isLeaving { - if let n = n { - node = .node(n) + _ = path.popLast() + continue + } + } else if case let .node(resultNode) = result { + edits.append((key ?? "root", resultNode)) + if !isLeaving { + if let resultNode = resultNode { + node = .node(resultNode) } else { _ = path.popLast() continue @@ -205,51 +195,55 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node } } -// if case .continue = result, isEdited { -// edits.append((key!, node!)) -// } + if case .continue = result, isEdited, case let .node(node) = node! { + edits.append((key!, node)) + } - if !isLeaving { + if isLeaving { + _ = path.popLast() + } else { stack = Stack(index: index, keys: keys, edits: edits, inArray: inArray, prev: stack) - inArray = node!.isArray - switch node! { case let .node(node): + inArray = false keys = visitorKeys[node.kind] ?? [] case let .array(array): - keys = array.map { _ in "root" } + inArray = true + keys = Array(array.indices) // array.map { _ in "root" } } - index = -1 edits = [] - if let parent = parent { ancestors.append(parent) } - parent = node } } while stack != nil - if !edits.isEmpty { - newRoot = edits[edits.count - 1].node + if !edits.isEmpty, let nextEditNode = edits[edits.count - 1].node { + return nextEditNode } - return newRoot + switch node! { + case let .node(root): // This should be equal to root, with any relevant edits + return root + case let .array(array): // This should never occur + return array[0] + } } final class Stack { let index: Int let keys: [IndexPathElement] - let edits: [(key: IndexPathElement, node: Node)] + let edits: [(key: IndexPathElement, node: Node?)] let inArray: Bool let prev: Stack? init( index: Int, keys: [IndexPathElement], - edits: [(key: IndexPathElement, node: Node)], + edits: [(key: IndexPathElement, node: Node?)], inArray: Bool, prev: Stack? ) { diff --git a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 858748d6..2b78e00a 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -2,5 +2,983 @@ import XCTest class VisitorTests: XCTestCase { - func test() throws {} + func testHandlesEmptyVisitor() throws { + let ast = try parse(source: "{ a }", noLocation: true) + XCTAssertNoThrow(visit(root: ast, visitor: .init())) + } + + func testValidatesPathArgument() throws { + var visited = [VisitedPath]() + let ast = try parse(source: "{ a }", noLocation: true) + + visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.enter, path)) + return .continue + }, + leave: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.leave, path)) + return .continue + } + )) + + XCTAssertEqual( + visited, + [ + .init(.enter, []), + .init(.enter, ["definitions", 0]), + .init(.enter, ["definitions", 0, "selectionSet"]), + .init(.enter, ["definitions", 0, "selectionSet", "selections", 0]), + .init(.enter, ["definitions", 0, "selectionSet", "selections", 0, "name"]), + .init(.leave, ["definitions", 0, "selectionSet", "selections", 0, "name"]), + .init(.leave, ["definitions", 0, "selectionSet", "selections", 0]), + .init(.leave, ["definitions", 0, "selectionSet"]), + .init(.leave, ["definitions", 0]), + .init(.leave, []), + ] + ) + } + + func testValidatesAncestorsArgument() throws { + var visited = [NodeResult]() + let ast = try parse(source: "{ a }", noLocation: true) + + visit(root: ast, visitor: .init( + enter: { node, _, parent, _, ancestors in + if let parent = parent, parent.isArray { + visited.append(parent) + } + visited.append(.node(node)) + + let expectedAncestors = visited[0 ... max(visited.count - 2, 0)] + XCTAssert(zip(ancestors, expectedAncestors).allSatisfy { lhs, rhs in + nodeResultsEqual(lhs, rhs) + }, "actual: \(ancestors), expected: \(expectedAncestors)") + return .continue + }, + leave: { _, _, parent, _, ancestors in + let expectedAncestors = visited[0 ... max(visited.count - 2, 0)] + XCTAssert(zip(ancestors, expectedAncestors).allSatisfy { lhs, rhs in + nodeResultsEqual(lhs, rhs) + }, "actual: \(ancestors), expected: \(expectedAncestors)") + + if let parent = parent, parent.isArray { + visited.removeLast() + } + visited.removeLast() + + return .continue + } + )) + } + + func testAllowsEditingANodeBothOnEnterAndOnLeave() throws { + let ast = try parse(source: "{ a, b, c { a, b, c } }", noLocation: true) + + var selectionSet: SelectionSet? = nil + + let editedASTNode = visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + if let node = node as? OperationDefinition { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + selectionSet = node.selectionSet + let newName = node.name + .map { Name(loc: $0.loc, value: $0.value + ".enter") } ?? + Name(value: "enter") + node.set(value: .node(newName), key: "name") + node.set(value: .node(SelectionSet(selections: [])), key: "selectionSet") + return .node(node) + } + return .continue + }, + leave: { node, key, parent, path, ancestors in + if let node = node as? OperationDefinition { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors, isEdited: true) + let newName = node.name + .map { Name(loc: $0.loc, value: $0.value + ".leave") } ?? + Name(value: "leave") + node.set(value: .node(newName), key: "name") + node.set(value: .node(selectionSet!), key: "selectionSet") + return .node(node) + } + return .continue + } + )) + + let editedAST = try XCTUnwrap(editedASTNode as? Document) + let operations = try XCTUnwrap(editedAST.definitions as? [OperationDefinition]) + XCTAssertEqual(operations.count, 1) + XCTAssertEqual(operations.first?.name?.value, "enter.leave") + let operationSelections = try XCTUnwrap(operations.first?.selectionSet.selections) + XCTAssertEqual(operationSelections.count, 3) + } + + func testAllowsEditingTheRootNodeOnEnterAndOnLeave() throws { + let ast = try parse(source: "{ a, b, c { a, b, c } }", noLocation: true) + + let editedASTNode = visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + if let node = node as? Document { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + var newDefinitions = node.definitions + newDefinitions.append( + DirectiveDefinition( + name: .init(value: "enter"), + locations: [.init(value: "root")] + ) + ) + node.set( + value: .array(newDefinitions), + key: "definitions" + ) + return .node(node) + } + return .continue + }, + leave: { node, key, parent, path, ancestors in + if let node = node as? Document { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors, isEdited: true) + var newDefinitions = node.definitions + newDefinitions.append( + DirectiveDefinition( + name: .init(value: "leave"), + locations: [.init(value: "root")] + ) + ) + node.set( + value: .array(newDefinitions), + key: "definitions" + ) + return .node(node) + } + return .continue + } + )) + + let editedAST = try XCTUnwrap(editedASTNode as? Document) + XCTAssertEqual(editedAST.definitions.count, 3) + try XCTAssertEqual( + XCTUnwrap(editedAST.definitions[1] as? DirectiveDefinition).name.value, + "enter" + ) + try XCTAssertEqual( + XCTUnwrap(editedAST.definitions[2] as? DirectiveDefinition).name.value, + "leave" + ) + } + + func testAllowsForEditingOnEnter() throws { + let ast = try parse(source: "{ a, b, c { a, b, c } }", noLocation: true) + + let editedASTNode = visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + if let node = node as? Field { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + if node.name.value == "b" { + return .node(nil) + } + } + return .continue + } + )) + + let editedAST = try XCTUnwrap(editedASTNode as? Document) + let operation = try XCTUnwrap(editedAST.definitions[0] as? OperationDefinition) + XCTAssertEqual( + operation.selectionSet.selections.count, + 2 // "b" is ignored + ) + + let cField = try XCTUnwrap(operation.selectionSet.selections[1] as? Field) + XCTAssertEqual( + cField.selectionSet?.selections.count, + 2 // "b" is ignored + ) + } + + func testAllowsForEditingOnLeave() throws { + let ast = try parse(source: "{ a, b, c { a, b, c } }", noLocation: true) + + let editedASTNode = visit(root: ast, visitor: .init( + leave: { node, key, parent, path, ancestors in + if let node = node as? Field { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + if node.name.value == "b" { + return .node(nil) + } + } + return .continue + } + )) + + let editedAST = try XCTUnwrap(editedASTNode as? Document) + let operation = try XCTUnwrap(editedAST.definitions[0] as? OperationDefinition) + XCTAssertEqual( + operation.selectionSet.selections.count, + 2 // "b" is removed + ) + + let cField = try XCTUnwrap(operation.selectionSet.selections[1] as? Field) + XCTAssertEqual( + cField.selectionSet?.selections.count, + 2 // "b" is removed + ) + } + + func testIgnoresSkipReturnedOnLeave() throws { + let ast = try parse(source: "{ a, b, c { a, b, c } }", noLocation: true) + + let editedASTNode = visit(root: ast, visitor: .init( + leave: { _, _, _, _, _ in + .skip // graphql-js 'false' is Swift '.skip' + } + )) + + let editedAST = try XCTUnwrap(editedASTNode as? Document) + let operation = try XCTUnwrap(editedAST.definitions[0] as? OperationDefinition) + XCTAssertEqual( + operation.selectionSet.selections.count, + 3 // "b" remains + ) + + let cField = try XCTUnwrap(operation.selectionSet.selections[2] as? Field) + XCTAssertEqual( + cField.selectionSet?.selections.count, + 3 // "b" remains + ) + } + + func testVisitsEditedNode() throws { + let addedField = Field( + name: Name(value: "__typename") + ) + + var didVisitAddedField = false + + let ast = try parse(source: "{ a { x } }", noLocation: true) + visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors, isEdited: true) + if let node = node as? Field, node.name.value == "a" { + if let selectionSet = node.selectionSet { + var newSelections = selectionSet.selections + newSelections.append(addedField) + + let newSelectionSet = selectionSet + newSelectionSet.set(value: .array(newSelections), key: "selections") + + let newNode = node + newNode.set(value: .node(newSelectionSet), key: "selectionSet") + return .node(newNode) + } + } + if let node = node as? Field, node.name.value == "__typename" { + didVisitAddedField = true + } + return .continue + } + )) + + XCTAssert(didVisitAddedField) + } + + func testAllowsSkippingASubTree() throws { + struct VisitedElement: Equatable { + let direction: VisitDirection + let kind: Kind + let value: String? + + init(_ direction: VisitDirection, _ kind: Kind, _ value: String?) { + self.direction = direction + self.kind = kind + self.value = value + } + } + + var visited = [VisitedElement]() + let ast = try parse(source: "{ a, b { x }, c }", noLocation: true) + + visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.enter, node.kind, getValue(node: node))) + if let node = node as? Field, node.name.value == "b" { + return .skip + } + return .continue + }, + leave: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.leave, node.kind, getValue(node: node))) + return .continue + } + )) + + XCTAssertEqual( + visited, + [ + .init(.enter, .document, nil), + .init(.enter, .operationDefinition, nil), + .init(.enter, .selectionSet, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "a"), + .init(.leave, .name, "a"), + .init(.leave, .field, nil), + .init(.enter, .field, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "c"), + .init(.leave, .name, "c"), + .init(.leave, .field, nil), + .init(.leave, .selectionSet, nil), + .init(.leave, .operationDefinition, nil), + .init(.leave, .document, nil), + ] + ) + } + + func testAllowsEarlyExitWhileVisiting() throws { + struct VisitedElement: Equatable { + let direction: VisitDirection + let kind: Kind + let value: String? + + init(_ direction: VisitDirection, _ kind: Kind, _ value: String?) { + self.direction = direction + self.kind = kind + self.value = value + } + } + + var visited = [VisitedElement]() + let ast = try parse(source: "{ a, b { x }, c }", noLocation: true) + + visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.enter, node.kind, getValue(node: node))) + if let node = node as? Name, node.value == "x" { + return .break + } + return .continue + }, + leave: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.leave, node.kind, getValue(node: node))) + return .continue + } + )) + + XCTAssertEqual( + visited, + [ + .init(.enter, .document, nil), + .init(.enter, .operationDefinition, nil), + .init(.enter, .selectionSet, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "a"), + .init(.leave, .name, "a"), + .init(.leave, .field, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "b"), + .init(.leave, .name, "b"), + .init(.enter, .selectionSet, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "x"), + ] + ) + } + + func testAllowsEarlyExitWhileLeaving() throws { + struct VisitedElement: Equatable { + let direction: VisitDirection + let kind: Kind + let value: String? + + init(_ direction: VisitDirection, _ kind: Kind, _ value: String?) { + self.direction = direction + self.kind = kind + self.value = value + } + } + + var visited = [VisitedElement]() + let ast = try parse(source: "{ a, b { x }, c }", noLocation: true) + + visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.enter, node.kind, getValue(node: node))) + return .continue + }, + leave: { node, key, parent, path, ancestors in + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.leave, node.kind, getValue(node: node))) + if let node = node as? Name, node.value == "x" { + return .break + } + return .continue + } + )) + + XCTAssertEqual( + visited, + [ + .init(.enter, .document, nil), + .init(.enter, .operationDefinition, nil), + .init(.enter, .selectionSet, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "a"), + .init(.leave, .name, "a"), + .init(.leave, .field, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "b"), + .init(.leave, .name, "b"), + .init(.enter, .selectionSet, nil), + .init(.enter, .field, nil), + .init(.enter, .name, "x"), + .init(.leave, .name, "x"), + ] + ) + } + + func testAllowsANamedFunctionsVisitorAPI() throws { + struct VisitedElement: Equatable { + let direction: VisitDirection + let kind: Kind + let value: String? + + init(_ direction: VisitDirection, _ kind: Kind, _ value: String?) { + self.direction = direction + self.kind = kind + self.value = value + } + } + + var visited = [VisitedElement]() + let ast = try parse(source: "{ a, b { x }, c }", noLocation: true) + + visit(root: ast, visitor: .init( + enter: { node, key, parent, path, ancestors in + if let node = node as? Name { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.enter, node.kind, getValue(node: node))) + } + if let node = node as? SelectionSet { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.enter, node.kind, getValue(node: node))) + } + return .continue + }, + leave: { node, key, parent, path, ancestors in + if let node = node as? SelectionSet { + checkVisitorFnArgs(ast, node, key, parent, path, ancestors) + visited.append(.init(.leave, node.kind, getValue(node: node))) + } + return .continue + } + )) + + XCTAssertEqual( + visited, + [ + .init(.enter, .selectionSet, nil), + .init(.enter, .name, "a"), + .init(.enter, .name, "b"), + .init(.enter, .selectionSet, nil), + .init(.enter, .name, "x"), + .init(.leave, .selectionSet, nil), + .init(.enter, .name, "c"), + .init(.leave, .selectionSet, nil), + ] + ) + } + + func testProperlyVisitsTheKitchenSinkQuery() throws { + var visited = [VisitedKindAndParent]() + + guard + let url = Bundle.module.url(forResource: "kitchen-sink", withExtension: "graphql"), + let kitchenSink = try? String(contentsOf: url) + else { + XCTFail("Could not load kitchen sink") + return + } + let ast = try parse(source: kitchenSink) + + visit(root: ast, visitor: .init( + enter: { node, key, parent, _, _ in + var parentKind: Kind? + if case let .node(parent) = parent { + parentKind = parent.kind + } + visited.append(.init(.enter, node.kind, key, parentKind)) + return .continue + }, + leave: { node, key, parent, _, _ in + var parentKind: Kind? + if case let .node(parent) = parent { + parentKind = parent.kind + } + visited.append(.init(.leave, node.kind, key, parentKind)) + + return .continue + } + )) + + XCTAssertEqual( + visited, + [ + .init(.enter, .document, nil, nil), + .init(.enter, .operationDefinition, 0, nil), + .init(.enter, .name, "name", .operationDefinition), + .init(.leave, .name, "name", .operationDefinition), + .init(.enter, .variableDefinition, 0, nil), + .init(.enter, .variable, "variable", .variableDefinition), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "variable", .variableDefinition), + .init(.enter, .namedType, "type", .variableDefinition), + .init(.enter, .name, "name", .namedType), + .init(.leave, .name, "name", .namedType), + .init(.leave, .namedType, "type", .variableDefinition), + .init(.leave, .variableDefinition, 0, nil), + .init(.enter, .variableDefinition, 1, nil), + .init(.enter, .variable, "variable", .variableDefinition), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "variable", .variableDefinition), + .init(.enter, .namedType, "type", .variableDefinition), + .init(.enter, .name, "name", .namedType), + .init(.leave, .name, "name", .namedType), + .init(.leave, .namedType, "type", .variableDefinition), + .init(.enter, .enumValue, "defaultValue", .variableDefinition), + .init(.leave, .enumValue, "defaultValue", .variableDefinition), + .init(.leave, .variableDefinition, 1, nil), + .init(.enter, .selectionSet, "selectionSet", .operationDefinition), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "alias", .field), + .init(.leave, .name, "alias", .field), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .listValue, "value", .argument), + .init(.enter, .intValue, 0, nil), + .init(.leave, .intValue, 0, nil), + .init(.enter, .intValue, 1, nil), + .init(.leave, .intValue, 1, nil), + .init(.leave, .listValue, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.enter, .inlineFragment, 1, nil), + .init(.enter, .namedType, "typeCondition", .inlineFragment), + .init(.enter, .name, "name", .namedType), + .init(.leave, .name, "name", .namedType), + .init(.leave, .namedType, "typeCondition", .inlineFragment), + .init(.enter, .directive, 0, nil), + .init(.enter, .name, "name", .directive), + .init(.leave, .name, "name", .directive), + .init(.leave, .directive, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .inlineFragment), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.enter, .field, 1, nil), + .init(.enter, .name, "alias", .field), + .init(.leave, .name, "alias", .field), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .intValue, "value", .argument), + .init(.leave, .intValue, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.enter, .argument, 1, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .variable, "value", .argument), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "value", .argument), + .init(.leave, .argument, 1, nil), + .init(.enter, .directive, 0, nil), + .init(.enter, .name, "name", .directive), + .init(.leave, .name, "name", .directive), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .variable, "value", .argument), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.leave, .directive, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.enter, .fragmentSpread, 1, nil), + .init(.enter, .name, "name", .fragmentSpread), + .init(.leave, .name, "name", .fragmentSpread), + .init(.leave, .fragmentSpread, 1, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 1, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .inlineFragment), + .init(.leave, .inlineFragment, 1, nil), + .init(.enter, .inlineFragment, 2, nil), + .init(.enter, .directive, 0, nil), + .init(.enter, .name, "name", .directive), + .init(.leave, .name, "name", .directive), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .variable, "value", .argument), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.leave, .directive, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .inlineFragment), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .inlineFragment), + .init(.leave, .inlineFragment, 2, nil), + .init(.enter, .inlineFragment, 3, nil), + .init(.enter, .selectionSet, "selectionSet", .inlineFragment), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .inlineFragment), + .init(.leave, .inlineFragment, 3, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .operationDefinition), + .init(.leave, .operationDefinition, 0, nil), + .init(.enter, .operationDefinition, 1, nil), + .init(.enter, .name, "name", .operationDefinition), + .init(.leave, .name, "name", .operationDefinition), + .init(.enter, .selectionSet, "selectionSet", .operationDefinition), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .intValue, "value", .argument), + .init(.leave, .intValue, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.enter, .directive, 0, nil), + .init(.enter, .name, "name", .directive), + .init(.leave, .name, "name", .directive), + .init(.leave, .directive, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .operationDefinition), + .init(.leave, .operationDefinition, 1, nil), + .init(.enter, .operationDefinition, 2, nil), + .init(.enter, .name, "name", .operationDefinition), + .init(.leave, .name, "name", .operationDefinition), + .init(.enter, .variableDefinition, 0, nil), + .init(.enter, .variable, "variable", .variableDefinition), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "variable", .variableDefinition), + .init(.enter, .namedType, "type", .variableDefinition), + .init(.enter, .name, "name", .namedType), + .init(.leave, .name, "name", .namedType), + .init(.leave, .namedType, "type", .variableDefinition), + .init(.leave, .variableDefinition, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .operationDefinition), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .variable, "value", .argument), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.enter, .field, 1, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .selectionSet, "selectionSet", .field), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 1, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .field), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .operationDefinition), + .init(.leave, .operationDefinition, 2, nil), + .init(.enter, .fragmentDefinition, 3, nil), + .init(.enter, .name, "name", .fragmentDefinition), + .init(.leave, .name, "name", .fragmentDefinition), + .init(.enter, .namedType, "typeCondition", .fragmentDefinition), + .init(.enter, .name, "name", .namedType), + .init(.leave, .name, "name", .namedType), + .init(.leave, .namedType, "typeCondition", .fragmentDefinition), + .init(.enter, .selectionSet, "selectionSet", .fragmentDefinition), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .variable, "value", .argument), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.enter, .argument, 1, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .variable, "value", .argument), + .init(.enter, .name, "name", .variable), + .init(.leave, .name, "name", .variable), + .init(.leave, .variable, "value", .argument), + .init(.leave, .argument, 1, nil), + .init(.enter, .argument, 2, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .objectValue, "value", .argument), + .init(.enter, .objectField, 0, nil), + .init(.enter, .name, "name", .objectField), + .init(.leave, .name, "name", .objectField), + .init(.enter, .stringValue, "value", .objectField), + .init(.leave, .stringValue, "value", .objectField), + .init(.leave, .objectField, 0, nil), + .init(.leave, .objectValue, "value", .argument), + .init(.leave, .argument, 2, nil), + .init(.leave, .field, 0, nil), + .init(.leave, .selectionSet, "selectionSet", .fragmentDefinition), + .init(.leave, .fragmentDefinition, 3, nil), + .init(.enter, .operationDefinition, 4, nil), + .init(.enter, .selectionSet, "selectionSet", .operationDefinition), + .init(.enter, .field, 0, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.enter, .argument, 0, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .booleanValue, "value", .argument), + .init(.leave, .booleanValue, "value", .argument), + .init(.leave, .argument, 0, nil), + .init(.enter, .argument, 1, nil), + .init(.enter, .name, "name", .argument), + .init(.leave, .name, "name", .argument), + .init(.enter, .booleanValue, "value", .argument), + .init(.leave, .booleanValue, "value", .argument), + .init(.leave, .argument, 1, nil), + .init(.leave, .field, 0, nil), + .init(.enter, .field, 1, nil), + .init(.enter, .name, "name", .field), + .init(.leave, .name, "name", .field), + .init(.leave, .field, 1, nil), + .init(.leave, .selectionSet, "selectionSet", .operationDefinition), + .init(.leave, .operationDefinition, 4, nil), + .init(.leave, .document, nil, nil), + ] + ) + } +} + +enum VisitDirection: Equatable { + case enter + case leave +} + +struct VisitedPath { + let direction: VisitDirection + let path: [IndexPathElement] + + init(_ direction: VisitDirection, _ path: [IndexPathElement]) { + self.direction = direction + self.path = path + } +} + +extension VisitedPath: Equatable { + static func == (lhs: VisitedPath, rhs: VisitedPath) -> Bool { + return lhs.direction == rhs.direction && + zip(lhs.path, rhs.path).allSatisfy { lhs, rhs in + lhs.description == rhs.description + } + } +} + +struct VisitedKindAndParent { + let direction: VisitDirection + let kind: Kind + let key: IndexPathElement? + let parentKind: Kind? + + init( + _ direction: VisitDirection, + _ kind: Kind, + _ key: IndexPathElement?, + _ parentKind: Kind? + ) { + self.direction = direction + self.kind = kind + self.key = key + self.parentKind = parentKind + } +} + +extension VisitedKindAndParent: Equatable { + static func == (lhs: VisitedKindAndParent, rhs: VisitedKindAndParent) -> Bool { + return lhs.direction == rhs.direction && + lhs.kind == rhs.kind && + lhs.key?.description == rhs.key?.description && + lhs.parentKind == rhs.parentKind + } +} + +extension VisitedKindAndParent: CustomDebugStringConvertible { + var debugDescription: String { + "(\(direction), \(kind), \(key.debugDescription), \(parentKind.debugDescription))" + } +} + +func checkVisitorFnArgs( + _ ast: Document, + _ node: Node, + _ key: IndexPathElement?, + _ parent: NodeResult?, + _ path: [IndexPathElement], + _ ancestors: [NodeResult], + isEdited: Bool = false +) { + guard let key = key else { + if !isEdited { + guard let node = node as? Document else { + XCTFail() + return + } + XCTAssertEqual(node, ast) + } + XCTAssertNil(parent) + XCTAssert(path.isEmpty) + XCTAssert(ancestors.isEmpty) + return + } + XCTAssertEqual(path.last?.indexPathValue, key.indexPathValue) + XCTAssertEqual(ancestors.count, path.count - 1) + + if !isEdited { + var currentNode = NodeResult.node(ast) + for (index, ancestor) in ancestors.enumerated() { + XCTAssert(nodeResultsEqual(ancestor, currentNode)) + guard let nextNode = currentNode.get(key: path[index]) else { + XCTFail() + return + } + currentNode = nextNode + } + guard let parent = parent else { + XCTFail() + return + } + XCTAssert(nodeResultsEqual(parent, currentNode)) + guard let parentNode = parent.get(key: key) else { + XCTFail() + return + } + XCTAssert(nodeResultsEqual(parentNode, .node(node))) + } +} + +func nodeResultsEqual(_ n1: NodeResult, _ n2: NodeResult) -> Bool { + switch n1 { + case let .node(n1): + switch n2 { + case let .node(n2): + return n1.kind == n2.kind && n1.loc == n2.loc + default: + return false + } + case let .array(n1): + switch n2 { + case let .array(n2): + return zip(n1, n2).allSatisfy { n1, n2 in + nodesEqual(n1, n2) + } + default: + return false + } + } +} + +func nodesEqual(_ n1: Node, _ n2: Node) -> Bool { + return n1.kind == n2.kind && n1.loc == n2.loc +} + +func getValue(node: Node) -> String? { + switch node { + case let node as IntValue: + return node.value + case let node as FloatValue: + return node.value + case let node as StringValue: + return node.value + case let node as BooleanValue: + return node.value.description + case let node as EnumValue: + return node.value + case let node as Name: + return node.value + default: + return nil + } }