diff --git a/Changelog.md b/Changelog.md index f5a96fd2..42df9b9d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added end-to-end tests for command-line interface. #199 by @MaxDesiatov and @mattt. +### Fixed + +- Fixed public extensions exposing nested code of all access levels. + #195 by @Tunous. + ## [1.0.0-beta.5] - 2020-09-29 ### Added diff --git a/Sources/SwiftDoc/Symbol.swift b/Sources/SwiftDoc/Symbol.swift index 02514f8e..1b376a11 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -43,7 +43,10 @@ public final class Symbol { if let `extension` = `extension`, `extension`.modifiers.contains(where: { $0.name == "public" }) { - return true + + return api.modifiers.allSatisfy { modifier in + modifier.detail != nil || (modifier.name != "internal" && modifier.name != "fileprivate" && modifier.name != "private") + } } if let symbol = context.compactMap({ $0 as? Symbol }).last, diff --git a/Tests/SwiftDocTests/InterfaceTypeTests.swift b/Tests/SwiftDocTests/InterfaceTypeTests.swift index 986e92e0..0c3db153 100644 --- a/Tests/SwiftDocTests/InterfaceTypeTests.swift +++ b/Tests/SwiftDocTests/InterfaceTypeTests.swift @@ -69,4 +69,140 @@ final class InterfaceTypeTests: XCTestCase { XCTAssertEqual(symbol.api.name, "A") } } + + func testFunctionsInPublicExtension() throws { + let source = #""" + public extension Int { + func a() {} + public func b() {} + internal func c() {} + fileprivate func d() {} + private func e() {} + } + """# + + let url = try temporaryFile(contents: source) + let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) + let module = Module(name: "Module", sourceFiles: [sourceFile]) + + XCTAssertEqual(sourceFile.symbols.count, 5) + XCTAssertTrue(sourceFile.symbols[0].isPublic, "Function `a()` should BE marked as public - its visibility is specified by extension") + XCTAssertTrue(sourceFile.symbols[1].isPublic, "Function `b()` should BE marked as public - its visibility is public") + XCTAssertFalse(sourceFile.symbols[2].isPublic, "Function `c()` should NOT be marked as public - its visibility is internal") + XCTAssertFalse(sourceFile.symbols[3].isPublic, "Function `d()` should NOT be marked as public - its visibility is fileprivate") + XCTAssertFalse(sourceFile.symbols[4].isPublic, "Function `e()` should NOT be marked as public - its visibility is private") + + XCTAssertEqual(module.interface.symbols.count, 2) + XCTAssertEqual(module.interface.symbols[0].name, "a()", "Function `a()` should be in documented interface") + XCTAssertEqual(module.interface.symbols[1].name, "b()", "Function `b()` should be in documented interface") + } + + func testComputedPropertiesInPublicExtension() throws { + let source = #""" + public extension Int { + var a: Int { 1 } + public var b: Int { 1 } + internal var c: Int { 1 } + fileprivate var d: Int { 1 } + private var e: Int { 1 } + } + """# + + let url = try temporaryFile(contents: source) + let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) + let module = Module(name: "Module", sourceFiles: [sourceFile]) + + XCTAssertEqual(sourceFile.symbols.count, 5) + XCTAssertTrue(sourceFile.symbols[0].isPublic, "Property `a` should BE marked as public - its visibility is specified by extension") + XCTAssertTrue(sourceFile.symbols[1].isPublic, "Property `b` should BE marked as public - its visibility is public") + XCTAssertFalse(sourceFile.symbols[2].isPublic, "Property `c` should NOT be marked as public - its visibility is internal") + XCTAssertFalse(sourceFile.symbols[3].isPublic, "Property `d` should NOT be marked as public - its visibility is fileprivate") + XCTAssertFalse(sourceFile.symbols[4].isPublic, "Property `e` should NOT be marked as public - its visibility is private") + + XCTAssertEqual(module.interface.symbols.count, 2) + XCTAssertEqual(module.interface.symbols[0].name, "a", "Property `a` should be in documented interface") + XCTAssertEqual(module.interface.symbols[1].name, "b", "Property `b` should be in documented interface") + } + + func testComputedPropertiesWithMultipleAccessModifiersInPublicExtension() throws { + let source = #""" + public extension Int { + internal(set) var a: Int { + get { 1 } + set {} + } + private(set) var b: Int { + get { 1 } + set {} + } + public internal(set) var c: Int { + get { 1 } + set {} + } + public fileprivate(set) var d: Int { + get { 1 } + set {} + } + public private(set) var e: Int { + get { 1 } + set {} + } + } + """# + + let url = try temporaryFile(contents: source) + let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) + let module = Module(name: "Module", sourceFiles: [sourceFile]) + + XCTAssertEqual(sourceFile.symbols.count, 5) + XCTAssertTrue(sourceFile.symbols[0].isPublic, "Property `a` should be marked as public - the visibility of its getter is public") + XCTAssertTrue(sourceFile.symbols[1].isPublic, "Property `b` should be marked as public - the visibility of its getter is public") + XCTAssertTrue(sourceFile.symbols[2].isPublic, "Property `c` should be marked as public - the visibility of its getter is public") + XCTAssertTrue(sourceFile.symbols[3].isPublic, "Property `d` should be marked as public - the visibility of its getter is public") + XCTAssertTrue(sourceFile.symbols[4].isPublic, "Property `e` should be marked as public - the visibility of its getter is public") + + XCTAssertEqual(module.interface.symbols.count, 5) + XCTAssertEqual(module.interface.symbols[0].name, "a", "Property `a` should be in documented interface") + XCTAssertEqual(module.interface.symbols[1].name, "b", "Property `b` should be in documented interface") + XCTAssertEqual(module.interface.symbols[2].name, "c", "Property `c` should be in documented interface") + XCTAssertEqual(module.interface.symbols[3].name, "d", "Property `d` should be in documented interface") + XCTAssertEqual(module.interface.symbols[4].name, "e", "Property `e` should be in documented interface") + } + + func testNestedPropertiesInPublicExtension() throws { + let source = #""" + public class RootController {} + + public extension RootController { + class ControllerExtension { + public var public_properties: ExtendedProperties = ExtendedProperties() + internal var internal_properties: InternalProperties = InternalProperties() + } + } + + public extension RootController.ControllerExtension { + struct ExtendedProperties { + public var public_prop: Int = 1 + } + } + + internal extension RootController.ControllerExtension { + struct InternalProperties { + internal var internal_prop: String = "FOO" + } + } + """# + + + let url = try temporaryFile(contents: source) + let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) + let module = Module(name: "Module", sourceFiles: [sourceFile]) + + XCTAssertEqual(module.interface.symbols.count, 5) + XCTAssertEqual(module.interface.symbols[0].name, "RootController") + XCTAssertEqual(module.interface.symbols[1].name, "ControllerExtension") + XCTAssertEqual(module.interface.symbols[2].name, "public_properties") + XCTAssertEqual(module.interface.symbols[3].name, "ExtendedProperties") + XCTAssertEqual(module.interface.symbols[4].name, "public_prop") + } }