From c0d2e25b3a1ea2ecab2ef350d52b97f6bc2ae544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Fri, 6 Jan 2023 16:56:53 -0800 Subject: [PATCH 1/7] Support more DirectiveArgumentWrapped configurations Also, generate more consistent `typeDisplayName` values --- .../DirectiveArgumentWrapper.swift | 246 +++++++++++++---- .../DirectiveArgumentWrappedTests.swift | 261 ++++++++++++++++++ 2 files changed, 448 insertions(+), 59 deletions(-) create mode 100644 Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift diff --git a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift index f4ed2a1a64..351fb0c4dd 100644 --- a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift +++ b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift @@ -89,130 +89,258 @@ public struct DirectiveArgumentWrapped: _DirectiveArgumentProtocol { fatalError() } + // Expected argument configurations + + @_disfavoredOverload + init( + wrappedValue: Value, + name: _DirectiveArgumentName = .inferredFromPropertyName, + parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), + allowedValues: [String]? = nil + ) { + self.init( + value: wrappedValue, + name: name, + transform: parseArgument, + allowedValues: allowedValues, + providedRequired: nil + ) + } + + @_disfavoredOverload + init( + name: _DirectiveArgumentName = .inferredFromPropertyName, + parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), + allowedValues: [String]? = nil + ) { + self.init( + value: nil, + name: name, + transform: parseArgument, + allowedValues: allowedValues, + providedRequired: nil + ) + } + private init( value: Value?, name: _DirectiveArgumentName, transform: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), allowedValues: [String]?, - required: Bool? + providedRequired: Bool? ) { + let required = providedRequired ?? (value == nil) + self.name = name self.defaultValue = value - if let optionallyWrappedValue = Value.self as? OptionallyWrapped.Type { - self.typeDisplayName = String(describing: optionallyWrappedValue.baseType()) + "?" - } else { - self.typeDisplayName = String(describing: Value.self) - } - - if let required = required { - self.required = required - } else { - self.required = defaultValue == nil - } - + self.typeDisplayName = typeDisplayNameDescription(defaultValue: value, required: required) self.parseArgument = transform self.allowedValues = allowedValues + self.required = required } + func setProperty( + on containingDirective: T, + named propertyName: String, + to any: Any + ) where T: AutomaticDirectiveConvertible { + let path = T.keyPaths[propertyName] as! ReferenceWritableKeyPath> + let wrappedValuePath = path.appending(path: \Self.parsedValue) + containingDirective[keyPath: wrappedValuePath] = any as! Value? + } + + // Warnings and errors for unexpected argument configurations + @_disfavoredOverload + @available(*, deprecated, message: "Use an optional type or a default value to control whether or not a directive argument is required.") init( wrappedValue: Value, name: _DirectiveArgumentName = .inferredFromPropertyName, parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), allowedValues: [String]? = nil, - required: Bool? = nil + required: Bool ) { self.init( value: wrappedValue, name: name, transform: parseArgument, allowedValues: allowedValues, - required: required + providedRequired: required ) } @_disfavoredOverload + @available(*, deprecated, message: "Use an optional type or a default value to control whether or not a directive argument is required.") init( name: _DirectiveArgumentName = .inferredFromPropertyName, parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), allowedValues: [String]? = nil, - required: Bool? = nil + required: Bool ) { self.init( value: nil, name: name, transform: parseArgument, allowedValues: allowedValues, - required: required + providedRequired: required ) } - - func setProperty( - on containingDirective: T, - named propertyName: String, - to any: Any - ) where T: AutomaticDirectiveConvertible { - let path = T.keyPaths[propertyName] as! ReferenceWritableKeyPath> - let wrappedValuePath = path.appending(path: \Self.parsedValue) - containingDirective[keyPath: wrappedValuePath] = any as! Value? - } } extension DirectiveArgumentWrapped where Value: DirectiveArgumentValueConvertible { + // Expected argument configurations + + @_disfavoredOverload init(name: _DirectiveArgumentName = .inferredFromPropertyName) { self.init(value: nil, name: name) } - init( - wrappedValue: Value, - name: _DirectiveArgumentName = .inferredFromPropertyName - ) { + @_disfavoredOverload + init(wrappedValue: Value, name: _DirectiveArgumentName = .inferredFromPropertyName) { self.init(value: wrappedValue, name: name) } - private init( - value: Value?, - name: _DirectiveArgumentName - ) { + private init(value: Value?, name: _DirectiveArgumentName) { self.name = name self.defaultValue = value - if let value = value { - self.typeDisplayName = String(describing: Value.self) + " = " + String(describing: value) - } else { - self.typeDisplayName = String(describing: Value.self) - } - + let required = value == nil + self.typeDisplayName = typeDisplayNameDescription(defaultValue: value, required: required) self.parseArgument = { _, argument in Value.init(rawDirectiveArgumentValue: argument) } self.allowedValues = Value.allowedValues() - self.required = value == nil + self.required = required + } + + // Warnings and errors for unexpected argument configurations + + @_disfavoredOverload + @available(*, unavailable, message: "Directive argument of non-optional types without default value need to be required. Use an optional type or provide a default value to make this argument non-required.") + init(name: _DirectiveArgumentName = .inferredFromPropertyName, required: Bool) { + fatalError() } } -protocol OptionallyWrappedDirectiveArgumentValueConvertible: OptionallyWrapped {} -extension Optional: OptionallyWrappedDirectiveArgumentValueConvertible where Wrapped: DirectiveArgumentValueConvertible {} -extension DirectiveArgumentWrapped where Value: OptionallyWrappedDirectiveArgumentValueConvertible { +protocol _OptionalDirectiveArgument { + associatedtype WrappedArgument: DirectiveArgumentValueConvertible + var wrapped: WrappedArgument? { get } + init(wrapping: WrappedArgument?) +} +extension Optional: _OptionalDirectiveArgument where Wrapped: DirectiveArgumentValueConvertible { + typealias WrappedArgument = Wrapped + var wrapped: WrappedArgument? { + switch self { + case .some(let value): + return value + case .none: return + nil + } + } + init(wrapping: WrappedArgument?) { + if let wrapped = wrapping { + self = .some(wrapped) + } else { + self = .none + } + } +} + +extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument { + // Expected argument configurations + + init(name: _DirectiveArgumentName = .inferredFromPropertyName) { + self = .init(value: nil, name: name) + } + + @_disfavoredOverload + init( + wrappedValue: Value, + name: _DirectiveArgumentName = .inferredFromPropertyName + ) { + self = .init(value: wrappedValue, name: name) + } + + @_disfavoredOverload + init( + name: _DirectiveArgumentName = .inferredFromPropertyName, + parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), + allowedValues: [String]? = nil + ) { + self = .init(value: nil, name: name, parseArgument: parseArgument, allowedValues: allowedValues) + } + + @_disfavoredOverload init( wrappedValue: Value, name: _DirectiveArgumentName = .inferredFromPropertyName, - required: Bool = false + parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), + allowedValues: [String]? = nil ) { - let argumentValueType = Value.baseType() as! DirectiveArgumentValueConvertible.Type + self = .init(value: wrappedValue, name: name, parseArgument: parseArgument, allowedValues: allowedValues) + } + + private init(value: Value?, name: _DirectiveArgumentName) { + let argumentValueType = Value.WrappedArgument.self + self = .init( + value: value, + name: name, + parseArgument: { _, argument in + Value(wrapping: argumentValueType.init(rawDirectiveArgumentValue: argument)) + }, + allowedValues: argumentValueType.allowedValues() + ) + } + + private init( + value: Value?, + name: _DirectiveArgumentName, + parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?), + allowedValues: [String]? = nil + ) { self.name = name - self.defaultValue = wrappedValue - if required { - self.typeDisplayName = String(describing: argumentValueType) - } else { - self.typeDisplayName = String(describing: argumentValueType) + "?" - } - - self.parseArgument = { _, argument in - argumentValueType.init(rawDirectiveArgumentValue: argument) - } - self.allowedValues = argumentValueType.allowedValues() - self.required = required + self.defaultValue = value + self.typeDisplayName = typeDisplayNameDescription(optionalDefaultValue: value, required: false) + self.parseArgument = parseArgument + self.allowedValues = allowedValues + self.required = false + } + + // Warnings and errors for unexpected argument configurations + + @_disfavoredOverload + @available(*, unavailable, message: "Directive arguments with an Optional type shouldn't be required.") + init( + name: _DirectiveArgumentName = .inferredFromPropertyName, + required: Bool + ) { + fatalError() + } + + @_disfavoredOverload + @available(*, unavailable, message: "Directive arguments with an Optional type shouldn't be required.") + init( + wrappedValue: Value, + name: _DirectiveArgumentName = .inferredFromPropertyName, + required: Bool + ) { + fatalError() } } + +private func typeDisplayNameDescription(defaultValue: Value?, required: Bool) -> String { + var name = "\(Value.self)" + + if let defaultValue = defaultValue { + name += " = \(defaultValue)" + } else if !required { + name += "?" + } + + return name +} + +private func typeDisplayNameDescription(optionalDefaultValue: Value?, required: Bool) -> String { + return typeDisplayNameDescription(defaultValue: optionalDefaultValue?.wrapped, required: required) +} diff --git a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift new file mode 100644 index 0000000000..b9c9723ed1 --- /dev/null +++ b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift @@ -0,0 +1,261 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation +import XCTest + +import Markdown + +@testable import SwiftDocC + +//@available(*, deprecated, message: "This test case is deprecated to silence other deprecation warnings. The tests still run.") +final class DirectiveArgumentWrappedTests: XCTestCase { + + // MARK: - Declarations + + // A custom type that + enum Something: String, CaseIterable, DirectiveArgumentValueConvertible { + case something + } + + // These are all declared directly on the test case so that the property wrappers can be easily accessed in the test. + + // MARK: Values + + // Without default values + + @DirectiveArgumentWrapped + var boolean: Bool + + @DirectiveArgumentWrapped + var number: Int + + @DirectiveArgumentWrapped + var customValue: Something + + // With default values + + @DirectiveArgumentWrapped + var booleanWithDefault: Bool = false + + @DirectiveArgumentWrapped + var numberWithDefault: Int = 0 + + @DirectiveArgumentWrapped + var customValueWithDefault: Something = .something + + // MARK: Optional values + + // Without default values + + @DirectiveArgumentWrapped + var optionalBoolean: Bool? + + @DirectiveArgumentWrapped + var optionalNumber: Int? + + @DirectiveArgumentWrapped + var optionalCustomValue: Something? + + // With default values + + @DirectiveArgumentWrapped + var optionalBooleanWithDefault: Bool? = true + + @DirectiveArgumentWrapped + var optionalNumberWithDefault: Int? = 0 + + @DirectiveArgumentWrapped + var optionalCustomValueWithDefault: Something? = .something + + // MARK: Explicit allowed values + + // Non-optional + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var booleanWithAllowedValues: Bool + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var numberWithAllowedValues: Int + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var customValueWithAllowedValues: Something + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var booleanWithAllowedValuesAndDefaultValue: Bool = false + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var numberWithAllowedValuesAndDefaultValue: Int = 0 + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var customValueWithAllowedValuesAndDefaultValue: Something = .something + + // Optional + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var optionalBooleanWithAllowedValues: Bool? + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var optionalNumberWithAllowedValues: Int? + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var optionalCustomValueWithAllowedValues: Something? + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var optionalBooleanWithAllowedValuesAndDefaultValue: Bool? = false + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var optionalNumberWithAllowedValuesAndDefaultValue: Int? = 0 + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in nil }, + allowedValues: ["one", "two", "three"]) + var optionalCustomValueWithAllowedValuesAndDefaultValue: Something? = .something + + // MARK: - Test assertions + + func testTypeDisplayName() throws { + + // MARK: Values + + // Without default values + + XCTAssertEqual(_boolean.typeDisplayName, "Bool") + XCTAssertEqual(_boolean.allowedValues, ["true", "false"]) + XCTAssertEqual(_boolean.required, true, "Argument without default value is required") + + XCTAssertEqual(_number.typeDisplayName, "Int") + XCTAssertEqual(_number.allowedValues, nil) + XCTAssertEqual(_number.required, true, "Argument without default value is required") + + XCTAssertEqual(_customValue.typeDisplayName, "Something") + XCTAssertEqual(_customValue.allowedValues, ["something"]) + XCTAssertEqual(_customValue.required, true, "Argument without default value is required") + + // With default values + + XCTAssertEqual(_booleanWithDefault.typeDisplayName, "Bool = false") + XCTAssertEqual(_booleanWithDefault.allowedValues, ["true", "false"]) + XCTAssertEqual(_booleanWithDefault.required, false, "Argument has default value to fallback to") + + XCTAssertEqual(_numberWithDefault.typeDisplayName, "Int = 0") + XCTAssertEqual(_numberWithDefault.allowedValues, nil) + XCTAssertEqual(_numberWithDefault.required, false, "Argument has default value to fallback to") + + XCTAssertEqual(_customValueWithDefault.typeDisplayName, "Something = something") + XCTAssertEqual(_customValueWithDefault.allowedValues, ["something"]) + XCTAssertEqual(_customValueWithDefault.required, false, "Argument has default value to fallback to") + + // MARK: Optional values + + XCTAssertEqual(_optionalBoolean.typeDisplayName, "Bool?") + XCTAssertEqual(_optionalBoolean.allowedValues, ["true", "false"]) + XCTAssertEqual(_optionalBoolean.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNumber.typeDisplayName, "Int?") + XCTAssertEqual(_optionalNumber.allowedValues, nil) + XCTAssertEqual(_optionalNumber.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalCustomValue.typeDisplayName, "Something?") + XCTAssertEqual(_optionalCustomValue.allowedValues, ["something"]) + XCTAssertEqual(_optionalCustomValue.required, false, "Argument with optional type is not required") + + // With default values + + XCTAssertEqual(_optionalBooleanWithDefault.typeDisplayName, "Bool = true") + XCTAssertEqual(_optionalBooleanWithDefault.allowedValues, ["true", "false"]) + XCTAssertEqual(_optionalBooleanWithDefault.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNumberWithDefault.typeDisplayName, "Int = 0") + XCTAssertEqual(_optionalNumberWithDefault.allowedValues, nil) + XCTAssertEqual(_optionalNumberWithDefault.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalCustomValueWithDefault.typeDisplayName, "Something = something") + XCTAssertEqual(_optionalCustomValueWithDefault.allowedValues, ["something"]) + XCTAssertEqual(_optionalCustomValueWithDefault.required, false, "Argument with optional type is not required") + + // MARK: Explicit allowed values + + // Non-optional + + XCTAssertEqual(_booleanWithAllowedValues.typeDisplayName, "Bool") + XCTAssertEqual(_booleanWithAllowedValues.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_booleanWithAllowedValues.required, true, "Argument without default value is required") + + XCTAssertEqual(_numberWithAllowedValues.typeDisplayName, "Int") + XCTAssertEqual(_numberWithAllowedValues.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_numberWithAllowedValues.required, true, "Argument without default value is required") + + XCTAssertEqual(_customValueWithAllowedValues.typeDisplayName, "Something") + XCTAssertEqual(_customValueWithAllowedValues.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_customValueWithAllowedValues.required, true, "Argument without default value is required") + + XCTAssertEqual(_booleanWithAllowedValuesAndDefaultValue.typeDisplayName, "Bool = false") + XCTAssertEqual(_booleanWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_booleanWithAllowedValuesAndDefaultValue.required, false, "Argument has default value to fallback to") + + XCTAssertEqual(_numberWithAllowedValuesAndDefaultValue.typeDisplayName, "Int = 0") + XCTAssertEqual(_numberWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_numberWithAllowedValuesAndDefaultValue.required, false, "Argument has default value to fallback to") + + XCTAssertEqual(_customValueWithAllowedValuesAndDefaultValue.typeDisplayName, "Something = something") + XCTAssertEqual(_customValueWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_customValueWithAllowedValuesAndDefaultValue.required, false, "Argument has default value to fallback to") + + // Optional + + XCTAssertEqual(_optionalBooleanWithAllowedValues.typeDisplayName, "Bool?") + XCTAssertEqual(_optionalBooleanWithAllowedValues.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_optionalBooleanWithAllowedValues.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNumberWithAllowedValues.typeDisplayName, "Int?") + XCTAssertEqual(_optionalNumberWithAllowedValues.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_optionalNumberWithAllowedValues.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalCustomValueWithAllowedValues.typeDisplayName, "Something?") + XCTAssertEqual(_optionalCustomValueWithAllowedValues.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_optionalCustomValueWithAllowedValues.required, false, "Argument with optional type is not required") + + + XCTAssertEqual(_optionalBooleanWithAllowedValuesAndDefaultValue.typeDisplayName, "Bool = false") + XCTAssertEqual(_optionalBooleanWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_optionalBooleanWithAllowedValuesAndDefaultValue.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNumberWithAllowedValuesAndDefaultValue.typeDisplayName, "Int = 0") + XCTAssertEqual(_optionalNumberWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_optionalNumberWithAllowedValuesAndDefaultValue.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalCustomValueWithAllowedValuesAndDefaultValue.typeDisplayName, "Something = something") + XCTAssertEqual(_optionalCustomValueWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") + XCTAssertEqual(_optionalCustomValueWithAllowedValuesAndDefaultValue.required, false, "Argument with optional type is not required") + } +} From 0f3706236f476d3c988918080c746a0ddcca813f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 8 Aug 2023 13:45:54 -0700 Subject: [PATCH 2/7] Remove commented out deprecation --- .../Infrastructure/DirectiveArgumentWrappedTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift index b9c9723ed1..957fa8a69b 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2023 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information @@ -15,7 +15,6 @@ import Markdown @testable import SwiftDocC -//@available(*, deprecated, message: "This test case is deprecated to silence other deprecation warnings. The tests still run.") final class DirectiveArgumentWrappedTests: XCTestCase { // MARK: - Declarations From 6bbc4c135d8f89cb4e5f19e8a8a67e7473f0c8d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 8 Aug 2023 15:51:08 -0700 Subject: [PATCH 3/7] Add tests for directive arguments with explicit nil default values --- .../DirectiveArgumentWrappedTests.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift index 957fa8a69b..f51257c7a0 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift @@ -74,6 +74,15 @@ final class DirectiveArgumentWrappedTests: XCTestCase { @DirectiveArgumentWrapped var optionalCustomValueWithDefault: Something? = .something + @DirectiveArgumentWrapped + var optionalBooleanWithNilDefault: Bool? = nil + + @DirectiveArgumentWrapped + var optionalNumberWithNilDefault: Int? = nil + + @DirectiveArgumentWrapped + var optionalCustomValueWithNilDefault: Something? = nil + // MARK: Explicit allowed values // Non-optional @@ -188,6 +197,20 @@ final class DirectiveArgumentWrappedTests: XCTestCase { XCTAssertEqual(_optionalCustomValue.allowedValues, ["something"]) XCTAssertEqual(_optionalCustomValue.required, false, "Argument with optional type is not required") + // With nil default values + + XCTAssertEqual(_optionalBooleanWithNilDefault.typeDisplayName, "Bool?") + XCTAssertEqual(_optionalBooleanWithNilDefault.allowedValues, ["true", "false"]) + XCTAssertEqual(_optionalBooleanWithNilDefault.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNumberWithNilDefault.typeDisplayName, "Int?") + XCTAssertEqual(_optionalNumberWithNilDefault.allowedValues, nil) + XCTAssertEqual(_optionalNumberWithNilDefault.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalCustomValueWithNilDefault.typeDisplayName, "Something?") + XCTAssertEqual(_optionalCustomValueWithNilDefault.allowedValues, ["something"]) + XCTAssertEqual(_optionalCustomValueWithNilDefault.required, false, "Argument with optional type is not required") + // With default values XCTAssertEqual(_optionalBooleanWithDefault.typeDisplayName, "Bool = true") From 24850a2f75189bb303e147a5904c77930c77fd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 8 Aug 2023 15:52:26 -0700 Subject: [PATCH 4/7] Fix type display name for non-directive-convertible optional values --- .../DirectiveArgumentWrapper.swift | 50 +++++++++-------- .../DirectiveArgumentWrappedTests.swift | 54 +++++++++++++++++++ 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift index 602623d0c9..78d5cf6192 100644 --- a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift +++ b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift @@ -243,11 +243,11 @@ extension DirectiveArgumentWrapped where Value: DirectiveArgumentValueConvertibl } protocol _OptionalDirectiveArgument { - associatedtype WrappedArgument: DirectiveArgumentValueConvertible + associatedtype WrappedArgument var wrapped: WrappedArgument? { get } init(wrapping: WrappedArgument?) } -extension Optional: _OptionalDirectiveArgument where Wrapped: DirectiveArgumentValueConvertible { +extension Optional: _OptionalDirectiveArgument { typealias WrappedArgument = Wrapped var wrapped: WrappedArgument? { switch self { @@ -266,8 +266,9 @@ extension Optional: _OptionalDirectiveArgument where Wrapped: DirectiveArgumentV } } -extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument { - // Expected argument configurations +extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument, Value.WrappedArgument: DirectiveArgumentValueConvertible { + + // When the wrapped value is DirectiveArgumentValueConvertible, additional arguments may be omitted init( name: _DirectiveArgumentName = .inferredFromPropertyName, @@ -285,6 +286,29 @@ extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument { self = .init(value: wrappedValue, name: name, hiddenFromDocumentation: hiddenFromDocumentation) } + private init( + value: Value?, + name: _DirectiveArgumentName, + hiddenFromDocumentation: Bool + ) { + let argumentValueType = Value.WrappedArgument.self + + self = .init( + value: value, + name: name, + parseArgument: { _, argument in + Value(wrapping: argumentValueType.init(rawDirectiveArgumentValue: argument)) + }, + allowedValues: argumentValueType.allowedValues(), + hiddenFromDocumentation: hiddenFromDocumentation + ) + } +} + +extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument { + + // Expected argument configurations + @_disfavoredOverload init( name: _DirectiveArgumentName = .inferredFromPropertyName, @@ -306,24 +330,6 @@ extension DirectiveArgumentWrapped where Value: _OptionalDirectiveArgument { self = .init(value: wrappedValue, name: name, parseArgument: parseArgument, allowedValues: allowedValues, hiddenFromDocumentation: hiddenFromDocumentation) } - private init( - value: Value?, - name: _DirectiveArgumentName, - hiddenFromDocumentation: Bool - ) { - let argumentValueType = Value.WrappedArgument.self - - self = .init( - value: value, - name: name, - parseArgument: { _, argument in - Value(wrapping: argumentValueType.init(rawDirectiveArgumentValue: argument)) - }, - allowedValues: argumentValueType.allowedValues(), - hiddenFromDocumentation: hiddenFromDocumentation - ) - } - private init( value: Value?, name: _DirectiveArgumentName, diff --git a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift index f51257c7a0..7daf5847ff 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift @@ -149,6 +149,38 @@ final class DirectiveArgumentWrappedTests: XCTestCase { allowedValues: ["one", "two", "three"]) var optionalCustomValueWithAllowedValuesAndDefaultValue: Something? = .something + // MARK: Non-convertible values + + enum SomethingNonConvertible: String { + case someValue + } + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in .someValue } + ) + var nonConvertibleValueWithCustomParsing: SomethingNonConvertible + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in .someValue } + ) + var nonConvertibleValueWithCustomParsingAndDefaultValue: SomethingNonConvertible = .someValue + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in .someValue } + ) + var optionalNonConvertibleValueWithCustomParsing: SomethingNonConvertible? + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in .someValue } + ) + var optionalNonConvertibleValueWithCustomParsingAndNilDefault: SomethingNonConvertible? = nil + + @DirectiveArgumentWrapped( + parseArgument: { _, _ in .someValue } + ) + var optionalNonConvertibleValueWithCustomParsingAndDefaultValue: SomethingNonConvertible? = .someValue + + // MARK: - Test assertions func testTypeDisplayName() throws { @@ -279,5 +311,27 @@ final class DirectiveArgumentWrappedTests: XCTestCase { XCTAssertEqual(_optionalCustomValueWithAllowedValuesAndDefaultValue.typeDisplayName, "Something = something") XCTAssertEqual(_optionalCustomValueWithAllowedValuesAndDefaultValue.allowedValues, ["one", "two", "three"], "Argument has explicitly specified allowed values") XCTAssertEqual(_optionalCustomValueWithAllowedValuesAndDefaultValue.required, false, "Argument with optional type is not required") + + // MARK: Non-convertible values + + XCTAssertEqual(_nonConvertibleValueWithCustomParsing.typeDisplayName, "SomethingNonConvertible") + XCTAssertEqual(_nonConvertibleValueWithCustomParsing.allowedValues, nil) + XCTAssertEqual(_nonConvertibleValueWithCustomParsing.required, true, "Argument without default value is required") + + XCTAssertEqual(_nonConvertibleValueWithCustomParsingAndDefaultValue.typeDisplayName, "SomethingNonConvertible = someValue") + XCTAssertEqual(_nonConvertibleValueWithCustomParsingAndDefaultValue.allowedValues, nil) + XCTAssertEqual(_nonConvertibleValueWithCustomParsingAndDefaultValue.required, false, "Argument with default value is not required") + + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsing.typeDisplayName, "SomethingNonConvertible?") + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsing.allowedValues, nil) + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsing.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsingAndNilDefault.typeDisplayName, "SomethingNonConvertible?") + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsingAndNilDefault.allowedValues, nil) + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsingAndNilDefault.required, false, "Argument with optional type is not required") + + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsingAndDefaultValue.typeDisplayName, "SomethingNonConvertible = someValue") + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsingAndDefaultValue.allowedValues, nil) + XCTAssertEqual(_optionalNonConvertibleValueWithCustomParsingAndDefaultValue.required, false, "Argument with optional type is not required") } } From f44cac9e2e3accbf18dd01e7606b914508a4e6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 8 Aug 2023 15:54:13 -0700 Subject: [PATCH 5/7] Update generated symbol graph to reflect recent documentation changes --- Sources/docc/DocCDocumentation.docc/DocC.symbols.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/docc/DocCDocumentation.docc/DocC.symbols.json b/Sources/docc/DocCDocumentation.docc/DocC.symbols.json index 666125b652..c1e1dcd5c2 100644 --- a/Sources/docc/DocCDocumentation.docc/DocC.symbols.json +++ b/Sources/docc/DocCDocumentation.docc/DocC.symbols.json @@ -3478,7 +3478,7 @@ "text" : "For example, use the page image directive to customize the icon used to represent this page in the navigation sidebar," }, { - "text" : "or the card image used to represent this page when using the ``Links`` directive and the ``Links\/detailedGrid``" + "text" : "or the card image used to represent this page when using the ``Links`` directive and the ``Links\/VisualStyle\/detailedGrid``" }, { "text" : "visual style." @@ -5112,7 +5112,7 @@ "text" : "" }, { - "text" : "@TitleHeading accepts an unnamed parameter containing containing the page's title heading." + "text" : "The `@TitleHeading` directive accepts an unnamed parameter containing containing the page's title heading." }, { "text" : "" From 84ac709da3002c1b824fafbd7a7aa4950722f7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 8 Aug 2023 16:02:30 -0700 Subject: [PATCH 6/7] Fix incomplete sentence in unit test comment --- .../Infrastructure/DirectiveArgumentWrappedTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift index 7daf5847ff..7e70584d25 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DirectiveArgumentWrappedTests.swift @@ -19,7 +19,7 @@ final class DirectiveArgumentWrappedTests: XCTestCase { // MARK: - Declarations - // A custom type that + // A custom type that is directive-argument-convertible enum Something: String, CaseIterable, DirectiveArgumentValueConvertible { case something } @@ -151,6 +151,7 @@ final class DirectiveArgumentWrappedTests: XCTestCase { // MARK: Non-convertible values + // A custom type that _isn't_ directive-argument-convertible enum SomethingNonConvertible: String { case someValue } From eeb1be866545b8bed62fa30fc7207dd0a07c2145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 8 Aug 2023 16:31:51 -0700 Subject: [PATCH 7/7] Update copyright year --- .../DirectiveInfrastructure/DirectiveArgumentWrapper.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift index 78d5cf6192..af602d7a96 100644 --- a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift +++ b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2023 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information