diff --git a/Sources/SwiftDoc/API.swift b/Sources/SwiftDoc/API.swift index dbdb7af2..aaa6cb0b 100644 --- a/Sources/SwiftDoc/API.swift +++ b/Sources/SwiftDoc/API.swift @@ -55,3 +55,9 @@ extension Subscript: API { extension Typealias: API {} extension Variable: API {} + +public enum AccessLevel: String, Codable { + case `public` + case `internal` + case `private` +} diff --git a/Sources/SwiftDoc/Interface.swift b/Sources/SwiftDoc/Interface.swift index 9370790d..774b05b6 100644 --- a/Sources/SwiftDoc/Interface.swift +++ b/Sources/SwiftDoc/Interface.swift @@ -5,10 +5,12 @@ import struct SwiftSemantics.Protocol public final class Interface: Codable { public let imports: [Import] public let symbols: [Symbol] + public let minimumAccessLevel: AccessLevel - public required init(imports: [Import], symbols: [Symbol]) { + public required init(imports: [Import], symbols: [Symbol], minimumAccessLevel: AccessLevel) { self.imports = imports - self.symbols = symbols.filter { $0.isPublic } + self.symbols = symbols.filter { $0.isIncluded(minimumAccessLevel: minimumAccessLevel) } + self.minimumAccessLevel = minimumAccessLevel } // MARK: - @@ -37,7 +39,7 @@ public final class Interface: Codable { var superclasses = Set(CollectionOfOne(baseClass)) while !superclasses.isEmpty { - let subclasses = Set(superclasses.flatMap { typesInheriting(from: $0) }.filter { $0.isPublic }) + let subclasses = Set(superclasses.flatMap { typesInheriting(from: $0) }.filter { $0.isIncluded(minimumAccessLevel: self.minimumAccessLevel) }) defer { superclasses = subclasses } classClusters[baseClass, default: []].formUnion(subclasses) } diff --git a/Sources/SwiftDoc/Module.swift b/Sources/SwiftDoc/Module.swift index 009346ed..a43063ac 100644 --- a/Sources/SwiftDoc/Module.swift +++ b/Sources/SwiftDoc/Module.swift @@ -7,18 +7,21 @@ public final class Module: Codable { public let sourceFiles: [SourceFile] + public let minimumAccessLevel: AccessLevel + public lazy var interface: Interface = { let imports = sourceFiles.flatMap { $0.imports } let symbols = sourceFiles.flatMap { $0.symbols } - return Interface(imports: imports, symbols: symbols) + return Interface(imports: imports, symbols: symbols, minimumAccessLevel: minimumAccessLevel) }() - public required init(name: String = "Anonymous", sourceFiles: [SourceFile]) { + public required init(name: String = "Anonymous", sourceFiles: [SourceFile], minimumAccessLevel: AccessLevel) { self.name = name self.sourceFiles = sourceFiles + self.minimumAccessLevel = minimumAccessLevel } - public convenience init(name: String = "Anonymous", paths: [String]) throws { + public convenience init(name: String = "Anonymous", paths: [String], minimumAccessLevel: AccessLevel) throws { var sources: [(file: URL, directory: URL)] = [] let fileManager = FileManager.default @@ -38,6 +41,6 @@ public final class Module: Codable { let sourceFiles = try sources.parallelMap { try SourceFile(file: $0.file, relativeTo: $0.directory) } - self.init(name: name, sourceFiles: sourceFiles) + self.init(name: name, sourceFiles: sourceFiles, minimumAccessLevel: minimumAccessLevel) } } diff --git a/Sources/SwiftDoc/Symbol.swift b/Sources/SwiftDoc/Symbol.swift index c41f5933..c5ce8dca 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -58,7 +58,26 @@ public final class Symbol { return false } - + + public var isInternal: Bool { + if api.modifiers.contains(where: { $0.name == "private" }) { + return false + } + + return true + } + + public func isIncluded(minimumAccessLevel: AccessLevel) -> Bool { + switch minimumAccessLevel { + case .public: + return isPublic + case .internal: + return isInternal + case .private: + return true + } + } + public var isDocumented: Bool { return documentation?.isEmpty == false } diff --git a/Sources/swift-doc/Extensions/DCOV+Extensions.swift b/Sources/swift-doc/Extensions/DCOV+Extensions.swift index fbd423cd..0fdf1f53 100644 --- a/Sources/swift-doc/Extensions/DCOV+Extensions.swift +++ b/Sources/swift-doc/Extensions/DCOV+Extensions.swift @@ -21,7 +21,7 @@ extension Report { public init(module: Module) { let entries = module.sourceFiles .flatMap { $0.symbols } - .filter { $0.isPublic } + .filter { $0.isIncluded(minimumAccessLevel: module.minimumAccessLevel) } .map { Entry($0) } self.init(entries: entries) diff --git a/Sources/swift-doc/Subcommands/Coverage.swift b/Sources/swift-doc/Subcommands/Coverage.swift index 4fbf4d7c..08312794 100644 --- a/Sources/swift-doc/Subcommands/Coverage.swift +++ b/Sources/swift-doc/Subcommands/Coverage.swift @@ -9,6 +9,11 @@ extension SwiftDoc { @Argument(help: "One or more paths to Swift files") var inputs: [String] + @Option(name: .customLong("minimum-access-level"), + default: .public, + help: "The minimum access level for declarations to be included") + var minimumAccessLevel: AccessLevel + @Option(name: .shortAndLong, help: "The path for generated report") var output: String? @@ -20,7 +25,7 @@ extension SwiftDoc { var options: Options func run() throws { - let module = try Module(paths: options.inputs) + let module = try Module(paths: options.inputs, minimumAccessLevel: options.minimumAccessLevel) let report = Report(module: module) if let output = options.output { diff --git a/Sources/swift-doc/Subcommands/Diagram.swift b/Sources/swift-doc/Subcommands/Diagram.swift index 50c72443..9e8f4d3d 100644 --- a/Sources/swift-doc/Subcommands/Diagram.swift +++ b/Sources/swift-doc/Subcommands/Diagram.swift @@ -11,6 +11,11 @@ extension SwiftDoc { struct Options: ParsableArguments { @Argument(help: "One or more paths to Swift files") var inputs: [String] + + @Option(name: .customLong("minimum-access-level"), + default: .public, + help: "The minimum access level for declarations to be included") + var minimumAccessLevel: AccessLevel } static var configuration = CommandConfiguration(abstract: "Generates diagram of Swift symbol relationships") @@ -19,7 +24,7 @@ extension SwiftDoc { var options: Options func run() throws { - let module = try Module(paths: options.inputs) + let module = try Module(paths: options.inputs, minimumAccessLevel: options.minimumAccessLevel) print(diagram(of: module), to: &standardOutput) } } @@ -61,7 +66,7 @@ fileprivate func diagram(of module: Module) -> String { } - for symbol in (module.interface.symbols.filter { $0.isPublic && $0.api is Type }) { + for symbol in (module.interface.symbols.filter { $0.isIncluded(minimumAccessLevel: module.minimumAccessLevel) && $0.api is Type }) { let symbolNode = Node("\(symbol.id)") graph.append(symbolNode) diff --git a/Sources/swift-doc/Subcommands/Generate.swift b/Sources/swift-doc/Subcommands/Generate.swift index 4901dcbc..fc599dfc 100644 --- a/Sources/swift-doc/Subcommands/Generate.swift +++ b/Sources/swift-doc/Subcommands/Generate.swift @@ -5,6 +5,8 @@ import SwiftMarkup import SwiftSemantics import struct SwiftSemantics.Protocol +extension AccessLevel: ExpressibleByArgument { } + extension SwiftDoc { struct Generate: ParsableCommand { enum Format: String, ExpressibleByArgument { @@ -30,6 +32,11 @@ extension SwiftDoc { help: "The output format") var format: Format + @Option(name: .customLong("minimum-access-level"), + default: .public, + help: "The minimum access level for declarations to be included") + var minimumAccessLevel: AccessLevel + @Option(name: .customLong("base-url"), default: "/", help: "The base URL used for all relative URLs in generated documents.") @@ -42,7 +49,7 @@ extension SwiftDoc { var options: Options func run() throws { - let module = try Module(name: options.moduleName, paths: options.inputs) + let module = try Module(name: options.moduleName, paths: options.inputs, minimumAccessLevel: options.minimumAccessLevel) let outputDirectoryURL = URL(fileURLWithPath: options.output) try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) @@ -53,7 +60,7 @@ extension SwiftDoc { var pages: [String: Page] = [:] var globals: [String: [Symbol]] = [:] - for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { + for symbol in module.interface.topLevelSymbols.filter({ $0.isIncluded(minimumAccessLevel: options.minimumAccessLevel) }) { switch symbol.api { case is Class, is Enumeration, is Structure, is Protocol: pages[path(for: symbol)] = TypePage(module: module, symbol: symbol) diff --git a/Sources/swift-doc/Supporting Types/Pages/HomePage.swift b/Sources/swift-doc/Supporting Types/Pages/HomePage.swift index b7a976d8..8e263aba 100644 --- a/Sources/swift-doc/Supporting Types/Pages/HomePage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/HomePage.swift @@ -18,7 +18,7 @@ struct HomePage: Page { init(module: Module) { self.module = module - for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { + for symbol in module.interface.topLevelSymbols.filter({ $0.isIncluded(minimumAccessLevel: module.minimumAccessLevel) }) { switch symbol.api { case is Class: classes.append(symbol) diff --git a/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift b/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift index 8c98b3af..29e8221c 100644 --- a/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift @@ -16,7 +16,7 @@ struct SidebarPage: Page { init(module: Module) { self.module = module - for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { + for symbol in module.interface.topLevelSymbols.filter({ $0.isIncluded(minimumAccessLevel: module.minimumAccessLevel) }) { switch symbol.api { case is Class: typeNames.insert(symbol.id.description) diff --git a/Tests/SwiftDocTests/NestedTypesTests.swift b/Tests/SwiftDocTests/NestedTypesTests.swift index ab2f18c8..35e31d26 100644 --- a/Tests/SwiftDocTests/NestedTypesTests.swift +++ b/Tests/SwiftDocTests/NestedTypesTests.swift @@ -23,7 +23,7 @@ final class NestedTypesTests: XCTestCase { let url = try temporaryFile(contents: source) let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) - let module = Module(name: "Module", sourceFiles: [sourceFile]) + let module = Module(name: "Module", sourceFiles: [sourceFile], minimumAccessLevel: .public) XCTAssertEqual(sourceFile.symbols.count, 4)