From a15c129bd68e3f27700a0392eceb9889adb916eb Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 8 Sep 2023 08:51:36 -0600 Subject: [PATCH 01/11] fix: Uncomments isEdited logic Also brings into alignment with graphql-js --- Sources/GraphQL/Language/Visitor.swift | 130 +++++++++++-------------- 1 file changed, 57 insertions(+), 73 deletions(-) diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 293fd113..403d99d8 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -88,72 +88,44 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node var keys: [IndexPathElement] = ["root"] var index: Int = -1 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 { + if isLeaving { key = ancestors.isEmpty ? nil : path.popLast() 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 = inArray ? index : keys[index] + + switch parent { + case let .node(parent): + node = parent.get(key: key!.keyValue!) + case let .array(parent): + node = .node(parent[key!.indexValue!]) + } - var result: VisitResult + if node == nil { + continue + } + path.append(key!) + } - if case let .node(n) = node! { + var result: VisitResult = .break // placeholder + if case let .node(n) = node { if !isLeaving { result = visitor.enter( node: n, @@ -188,15 +173,16 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node break } - if case .skip = result, !isLeaving { - _ = path.popLast() - continue - } else if case let .node(n) = result { - edits.append((key!, n!)) - + 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!, resultNode!)) + if !isLeaving { + if let resultNode = resultNode { + node = .node(resultNode) } else { _ = path.popLast() continue @@ -205,38 +191,36 @@ 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): keys = visitorKeys[node.kind] ?? [] case let .array(array): keys = 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 + return edits[edits.count - 1].node } - return newRoot + return root } final class Stack { From a3136ac30eedebecabb28e0769a2f7998d90b649 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 12 Sep 2023 23:36:03 -0600 Subject: [PATCH 02/11] fix: node nullability safety --- Sources/GraphQL/Language/Visitor.swift | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 403d99d8..6b93af62 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -87,7 +87,7 @@ 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? @@ -111,10 +111,15 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node let editKey = editKey.indexValue! let arrayKey = editKey - editOffset - if case .array(var n) = node { - n.remove(at: arrayKey) - node = .array(n) - editOffset += 1 + if case var .array(n) = node { + if let editValue = editValue { + n[arrayKey] = editValue + node = .array(n) + } else { + n.remove(at: arrayKey) + node = .array(n) + editOffset += 1 + } } } } else { @@ -179,7 +184,7 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node continue } } else if case let .node(resultNode) = result { - edits.append((key!, resultNode!)) + edits.append((key!, resultNode)) if !isLeaving { if let resultNode = resultNode { node = .node(resultNode) @@ -216,8 +221,8 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node } while stack != nil - if !edits.isEmpty { - return edits[edits.count - 1].node + if !edits.isEmpty, let nextEditNode = edits[edits.count - 1].node { + return nextEditNode } return root @@ -226,14 +231,14 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node 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? ) { From 478d2b3c6ae4bfb4e0fddf8f759624d92954ada1 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 8 Nov 2023 00:39:24 -0700 Subject: [PATCH 03/11] fix: Fixes visitor path generation bug --- Sources/GraphQL/Language/Visitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 6b93af62..ce014040 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -100,7 +100,7 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node let isEdited = isLeaving && !edits.isEmpty if isLeaving { - key = ancestors.isEmpty ? nil : path.popLast() + key = ancestors.isEmpty ? nil : path.last node = parent parent = ancestors.popLast() From 9a31a89d6a86dedfc830969f5d1b6e3194ea7aeb Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 9 Nov 2023 12:25:54 -0700 Subject: [PATCH 04/11] fix: Visitor correctly visits Directives, ListType, & ListValue --- Sources/GraphQL/Language/AST.swift | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 44c285c0..39524816 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -921,6 +921,15 @@ public final class ListValue { self.loc = loc self.values = values } + + public func get(key: String) -> NodeResult? { + switch key { + case "values": + return .array(values) + default: + return nil + } + } } extension ListValue: Equatable { @@ -1007,6 +1016,17 @@ public final class Directive { 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 + } + } } extension Directive: Equatable { @@ -1077,6 +1097,15 @@ public final class ListType { self.loc = loc self.type = type } + + public func get(key: String) -> NodeResult? { + switch key { + case "type": + return .node(type) + default: + return nil + } + } } extension ListType: Equatable { From 47267e67f81aa6c498f361f0daf8ab48782bf3a4 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 8 Nov 2023 00:38:48 -0700 Subject: [PATCH 05/11] test: Adds non-editing visitor tests --- Sources/GraphQL/Error/GraphQLError.swift | 4 +- Sources/GraphQL/Language/AST.swift | 17 +- Sources/GraphQL/Language/Kinds.swift | 2 +- .../LanguageTests/VisitorTests.swift | 816 +++++++++++++++++- 4 files changed, 834 insertions(+), 5 deletions(-) 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 39524816..089ce585 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,21 @@ 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]) + } + } } /** 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/Tests/GraphQLTests/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 858748d6..33fb7dc3 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -2,5 +2,819 @@ 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 { + struct VisitedElement: Equatable { + let direction: VisitDirection + let path: [any IndexPathElement] + + init(_ direction: VisitDirection, _ path: [any IndexPathElement]) { + self.direction = direction + self.path = path + } + + static func == (lhs: VisitedElement, rhs: VisitedElement) -> Bool { + return lhs.direction == rhs.direction && + zip(lhs.path, rhs.path).allSatisfy { lhs, rhs in + lhs.description == rhs.description + } + } + } + + var visited = [VisitedElement]() + 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 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 { + struct VisitedElement: Equatable, CustomDebugStringConvertible { + var debugDescription: String { + "(\(direction), \(kind), \(key.debugDescription), \(parentKind.debugDescription))" + } + + let direction: VisitDirection + let kind: Kind + let key: (any IndexPathElement)? + let parentKind: Kind? + + init( + _ direction: VisitDirection, + _ kind: Kind, + _ key: (any IndexPathElement)?, + _ parentKind: Kind? + ) { + self.direction = direction + self.kind = kind + self.key = key + self.parentKind = parentKind + } + + static func == (lhs: VisitedElement, rhs: VisitedElement) -> Bool { + return lhs.direction == rhs.direction && + lhs.kind == rhs.kind && + lhs.key?.description == rhs.key?.description && + lhs.parentKind == rhs.parentKind + } + } + var visited = [VisitedElement]() + + 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 + } } From 3ed9d6ee5a9d6068c4a43c2ddbde8e7c89dd159d Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 9 Nov 2023 12:28:57 -0700 Subject: [PATCH 06/11] feature: Adds visitor editing support --- Sources/GraphQL/Language/AST.swift | 98 ++++++++++++++++++++++++-- Sources/GraphQL/Language/Visitor.swift | 45 ++++++------ 2 files changed, 116 insertions(+), 27 deletions(-) diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 089ce585..62d87d91 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -174,6 +174,28 @@ public enum NodeResult { 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) + } + } + } } /** @@ -183,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 { @@ -191,7 +213,9 @@ public extension Node { return nil } - func set(value _: Node?, key _: String) {} + func set(value: NodeResult?, key: String) { + print("TODO: Should be implemented on each type!") + } } extension Name: Node {} @@ -252,7 +276,7 @@ extension Name: Equatable { public final class Document { public let kind: Kind = .document public let loc: Location? - public let definitions: [Definition] + public var definitions: [Definition] init(loc: Location? = nil, definitions: [Definition]) { self.loc = loc @@ -270,6 +294,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 { @@ -323,10 +365,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 var name: Name? + public var variableDefinitions: [VariableDefinition] + public var directives: [Directive] + public var selectionSet: SelectionSet init( loc: Location? = nil, @@ -364,6 +406,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 { diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index ce014040..9c944df2 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -123,14 +123,22 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node } } } else { - let clone = node - node = clone - for (editKey, editValue) in edits { - if case .node(let node) = node { - node.set(value: editValue, key: editKey.keyValue!) + if case let .node(n) = node { + for (editKey, editValue) in edits { + if let editValue = editValue { + if let key = editKey.keyValue { + n.set(value: .node(editValue), key: key) + } + } } + node = .node(n) } } + + // Since Swift cannot mutate node in-place, we must pass the changes up to parent. + if let key = key, let node = node { + parent = parent?.set(value: node, key: key) + } } index = stack!.index @@ -139,15 +147,8 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node inArray = stack!.inArray stack = stack!.prev } else if let parent = parent { - key = inArray ? index : keys[index] - - switch parent { - case let .node(parent): - node = parent.get(key: key!.keyValue!) - case let .array(parent): - node = .node(parent[key!.indexValue!]) - } - + key = keys[index] + node = parent.get(key: key!) if node == nil { continue } @@ -176,9 +177,7 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node if case .break = result { break - } - - if case .skip = result { + } else if case .skip = result { if !isLeaving { _ = path.popLast() continue @@ -204,12 +203,13 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node _ = 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 = [] @@ -225,7 +225,12 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node return nextEditNode } - return root + 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 { From 9e1657d3e11ffaf44ad644689f66d9bb4cb80d55 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 9 Nov 2023 13:24:56 -0700 Subject: [PATCH 07/11] fix: Root editing support in visitor --- Sources/GraphQL/Language/Visitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 9c944df2..27d7a604 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -183,7 +183,7 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node continue } } else if case let .node(resultNode) = result { - edits.append((key!, resultNode)) + edits.append((key ?? "root", resultNode)) if !isLeaving { if let resultNode = resultNode { node = .node(resultNode) From 1554f793e8db1ce38cece9d25d492aa7d062aef6 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 9 Nov 2023 14:05:10 -0700 Subject: [PATCH 08/11] feature: Adds visitor editing to most AST types --- Sources/GraphQL/Language/AST.swift | 466 ++++++++++++++++++++++++++--- 1 file changed, 428 insertions(+), 38 deletions(-) diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 62d87d91..d4271541 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -213,7 +213,7 @@ public extension Node { return nil } - func set(value: NodeResult?, key: String) { + func set(value _: NodeResult?, key _: String) { print("TODO: Should be implemented on each type!") } } @@ -276,7 +276,7 @@ extension Name: Equatable { public final class Document { public let kind: Kind = .document public let loc: Location? - public var definitions: [Definition] + public private(set) var definitions: [Definition] init(loc: Location? = nil, definitions: [Definition]) { self.loc = loc @@ -294,7 +294,7 @@ public final class Document { return nil } } - + public func set(value: NodeResult?, key: String) { guard let value = value else { return @@ -365,10 +365,10 @@ public final class OperationDefinition { public let kind: Kind = .operationDefinition public let loc: Location? public let operation: OperationType - public var name: Name? - public var variableDefinitions: [VariableDefinition] - public var directives: [Directive] - public var 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, @@ -406,7 +406,7 @@ public final class OperationDefinition { return nil } } - + public func set(value: NodeResult?, key: String) { guard let value = value else { return @@ -467,9 +467,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 @@ -490,6 +490,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 { @@ -517,7 +551,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 @@ -532,6 +566,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 { @@ -543,7 +595,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 @@ -561,6 +613,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 { @@ -612,11 +682,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, @@ -656,6 +726,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 { @@ -671,8 +791,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 @@ -690,6 +810,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 { @@ -706,8 +852,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 @@ -728,6 +874,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 { @@ -756,9 +928,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, @@ -789,6 +961,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 { @@ -802,10 +1008,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, @@ -838,6 +1044,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 { @@ -1014,7 +1262,7 @@ 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 @@ -1029,6 +1277,24 @@ public final class ListValue { 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 { @@ -1050,7 +1316,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 @@ -1065,6 +1331,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 { @@ -1076,8 +1360,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 @@ -1095,6 +1379,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 { @@ -1107,8 +1417,8 @@ 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 @@ -1126,6 +1436,32 @@ public final class Directive { 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 { @@ -1164,7 +1500,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 @@ -1179,6 +1515,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 { @@ -1190,7 +1544,7 @@ 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 @@ -1205,6 +1559,24 @@ public final class ListType { 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 { @@ -1220,7 +1592,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 @@ -1235,6 +1607,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 { From 07ad62b3830cb3e7c8997f6b298d7eb262445e45 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 8 Nov 2023 01:00:51 -0700 Subject: [PATCH 09/11] test: Adds editing visitor tests --- .../LanguageTests/VisitorTests.swift | 212 +++++++++++++++++- 1 file changed, 211 insertions(+), 1 deletion(-) diff --git a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 33fb7dc3..779c0ea9 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -90,7 +90,217 @@ class VisitorTests: XCTestCase { } )) } - + + 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 From c1cae120b07280ab435c68aaab2e83761cf47a8c Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 9 Nov 2023 15:52:37 -0700 Subject: [PATCH 10/11] fix: Reformat Visitor types to support old Swift versions --- .../LanguageTests/VisitorTests.swift | 50 +------------------ 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 779c0ea9..2b78e00a 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -8,24 +8,7 @@ class VisitorTests: XCTestCase { } func testValidatesPathArgument() throws { - struct VisitedElement: Equatable { - let direction: VisitDirection - let path: [any IndexPathElement] - - init(_ direction: VisitDirection, _ path: [any IndexPathElement]) { - self.direction = direction - self.path = path - } - - static func == (lhs: VisitedElement, rhs: VisitedElement) -> Bool { - return lhs.direction == rhs.direction && - zip(lhs.path, rhs.path).allSatisfy { lhs, rhs in - lhs.description == rhs.description - } - } - } - - var visited = [VisitedElement]() + var visited = [VisitedPath]() let ast = try parse(source: "{ a }", noLocation: true) visit(root: ast, visitor: .init( @@ -513,36 +496,7 @@ class VisitorTests: XCTestCase { } func testProperlyVisitsTheKitchenSinkQuery() throws { - struct VisitedElement: Equatable, CustomDebugStringConvertible { - var debugDescription: String { - "(\(direction), \(kind), \(key.debugDescription), \(parentKind.debugDescription))" - } - - let direction: VisitDirection - let kind: Kind - let key: (any IndexPathElement)? - let parentKind: Kind? - - init( - _ direction: VisitDirection, - _ kind: Kind, - _ key: (any IndexPathElement)?, - _ parentKind: Kind? - ) { - self.direction = direction - self.kind = kind - self.key = key - self.parentKind = parentKind - } - - static func == (lhs: VisitedElement, rhs: VisitedElement) -> Bool { - return lhs.direction == rhs.direction && - lhs.kind == rhs.kind && - lhs.key?.description == rhs.key?.description && - lhs.parentKind == rhs.parentKind - } - } - var visited = [VisitedElement]() + var visited = [VisitedKindAndParent]() guard let url = Bundle.module.url(forResource: "kitchen-sink", withExtension: "graphql"), From eae17e1599f335b5acc51fb34ce89dd9f6aac92b Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 10 Nov 2023 12:43:35 -0700 Subject: [PATCH 11/11] fix: Backsupport to prevent major version bump --- Sources/GraphQL/Language/AST.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index d4271541..3301e621 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -213,8 +213,13 @@ public extension Node { return nil } + @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) { - print("TODO: Should be implemented on each type!") + // This should be overridden by each type on which it should do something } }