Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Add option for specifying which access levels are included #219

Merged
merged 12 commits into from
Feb 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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.
- Added `--minimum-access-level` option to `generate` and `coverage` commands.
#219 by @Lukas-Stuehrk.

### Fixed

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ $ apt-get install -y libxml2-dev graphviz
-f, --format <format> The output format (default: commonmark)
--base-url <base-url> The base URL used for all relative URLs in generated
documents. (default: /)
--minimum-access-level <minimum-access-level>
The minimum access level of the symbols which should
be included. (default: public)
-h, --help Show help information.

The `generate` subcommand
Expand Down Expand Up @@ -131,6 +134,12 @@ $ Documentation/
└── index.html
```

By default,
`swift-doc` includes only symbols declared as `public` or `open`
in the generated documentation.
To include `internal` or `private` declarations,
pass the `--minimum-access-level` flag with the specified access level.

#### swift-doc coverage

OVERVIEW: Generates documentation coverage statistics for Swift files
Expand All @@ -142,6 +151,9 @@ $ Documentation/

OPTIONS:
-o, --output <output> The path for generated report
--minimum-access-level <minimum-access-level>
The minimum access level of the symbols which should
be included. (default: public)
-h, --help Show help information.

The `coverage` subcommand
Expand Down Expand Up @@ -190,6 +202,9 @@ please reach out by [opening an Issue][open an issue]!
<inputs> One or more paths to Swift files

OPTIONS:
--minimum-access-level <minimum-access-level>
The minimum access level of the symbols which should
be included. (default: public)
-h, --help Show help information.

The `diagram` subcommand
Expand Down
3 changes: 1 addition & 2 deletions Sources/SwiftDoc/Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ public final class Interface {

public required init(imports: [Import], symbols: [Symbol]) {
self.imports = imports
self.symbols = symbols.filter { $0.isPublic }
self.symbols = symbols

self.symbolsGroupedByIdentifier = Dictionary(grouping: symbols, by: { $0.id })
self.symbolsGroupedByQualifiedName = Dictionary(grouping: symbols, by: { $0.id.description })
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.pathComponents.isEmpty }

self.relationships = {
let symbols = symbols.filter { $0.isPublic }
let extensionsByExtendedType: [String: [Extension]] = Dictionary(grouping: symbols.flatMap { $0.context.compactMap { $0 as? Extension } }, by: { $0.extendedType })

var relationships: Set<Relationship> = []
Expand Down
38 changes: 38 additions & 0 deletions Sources/SwiftDoc/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,44 @@ public final class Symbol {
return false
}

public var isPrivate: Bool {
// We always assume that `Unknown` is public.
guard api is Unknown == false else {
return false
}

if api.modifiers.contains(where: { $0.detail == nil && ($0.name == "private" || $0.name == "fileprivate") }) {
return true
}

if let `extension` = `extension`,
`extension`.modifiers.contains(where: { $0.name == "private" || $0.name == "fileprivate" }) {

return api.modifiers.allSatisfy { modifier in
modifier.detail != nil || (modifier.name != "internal" && modifier.name != "public" && modifier.name != "open")
}
}

if let symbol = context.compactMap({ $0 as? Symbol }).last,
symbol.api.modifiers.contains(where: { $0.name == "private" || $0.name == "fileprivate" })
{
switch symbol.api {
case is Enumeration:
return api is Enumeration.Case
case is Protocol:
return api is Function || api is Variable
default:
break
}
}

return false
}

public var isInternal: Bool {
!isPublic && !isPrivate
}

public var isDocumented: Bool {
return documentation?.isEmpty == false
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/swift-doc/Extensions/DCOV+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ extension Entry {
// MARK: -

extension Report {
public init(module: Module) {
public init(module: Module, symbolFilter: (Symbol) -> Bool) {
let entries = module.sourceFiles
.flatMap { $0.symbols }
.filter { $0.isPublic }
.filter(symbolFilter)
.map { Entry($0) }

self.init(entries: entries)
Expand Down
6 changes: 5 additions & 1 deletion Sources/swift-doc/Subcommands/Coverage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ extension SwiftDoc {
@Option(name: .shortAndLong,
help: "The path for generated report")
var output: String?

@Option(name: .long,
help: "The minimum access level of the symbols considered for coverage statistics.")
var minimumAccessLevel: AccessLevel = .public
}

static var configuration = CommandConfiguration(abstract: "Generates documentation coverage statistics for Swift files")
Expand All @@ -21,7 +25,7 @@ extension SwiftDoc {

func run() throws {
let module = try Module(paths: options.inputs)
let report = Report(module: module)
let report = Report(module: module, symbolFilter: options.minimumAccessLevel.includes(symbol:))

if let output = options.output {
let encoder = JSONEncoder()
Expand Down
12 changes: 8 additions & 4 deletions Sources/swift-doc/Subcommands/Diagram.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ extension SwiftDoc {
struct Options: ParsableArguments {
@Argument(help: "One or more paths to Swift files")
var inputs: [String]

@Option(name: .long,
help: "The minimum access level of the symbols included in the generated diagram.")
var minimumAccessLevel: AccessLevel = .public
}

static var configuration = CommandConfiguration(abstract: "Generates diagram of Swift symbol relationships")

@OptionGroup()
var options: Options

func run() throws {
let module = try Module(paths: options.inputs)
print(diagram(of: module), to: &standardOutput)
print(diagram(of: module, including: options.minimumAccessLevel.includes(symbol:)), to: &standardOutput)
}
}
}

// MARK: -

fileprivate func diagram(of module: Module) -> String {
fileprivate func diagram(of module: Module, including symbolFilter: (Symbol) -> Bool) -> String {
var graph = Graph(directed: true)

for (baseClass, subclasses) in module.interface.classHierarchies {
Expand Down Expand Up @@ -61,7 +65,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.api is Type }).filter(symbolFilter) {
let symbolNode = Node("\(symbol.id)")
graph.append(symbolNode)

Expand Down
18 changes: 13 additions & 5 deletions Sources/swift-doc/Subcommands/Generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ extension SwiftDoc {
@Option(name: .customLong("base-url"),
help: "The base URL used for all relative URLs in generated documents.")
var baseURL: String = "/"

@Option(name: .long,
help: "The minimum access level of the symbols included in generated documentation.")
var minimumAccessLevel: AccessLevel = .public
}

static var configuration = CommandConfiguration(abstract: "Generates Swift documentation")
Expand All @@ -55,10 +59,11 @@ extension SwiftDoc {
var pages: [String: Page] = [:]

var globals: [String: [Symbol]] = [:]
for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) {
let symbolFilter = options.minimumAccessLevel.includes(symbol:)
for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
switch symbol.api {
case is Class, is Enumeration, is Structure, is Protocol:
pages[route(for: symbol)] = TypePage(module: module, symbol: symbol, baseURL: baseURL)
pages[route(for: symbol)] = TypePage(module: module, symbol: symbol, baseURL: baseURL, includingChildren: symbolFilter)
case let `typealias` as Typealias:
pages[route(for: `typealias`.name)] = TypealiasPage(module: module, symbol: symbol, baseURL: baseURL)
case let function as Function where !function.isOperator:
Expand All @@ -76,6 +81,9 @@ extension SwiftDoc {

guard !pages.isEmpty else {
logger.warning("No public API symbols were found at the specified path. No output was written.")
if options.minimumAccessLevel == .public {
logger.warning("By default, swift-doc only includes public declarations. Maybe you want to use --minimum-access-level to include non-public declarations?")
}
return
}

Expand All @@ -93,11 +101,11 @@ extension SwiftDoc {
} else {
switch format {
case .commonmark:
pages["Home"] = HomePage(module: module, baseURL: baseURL)
pages["_Sidebar"] = SidebarPage(module: module, baseURL: baseURL)
pages["Home"] = HomePage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
pages["_Sidebar"] = SidebarPage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
pages["_Footer"] = FooterPage(baseURL: baseURL)
case .html:
pages["Home"] = HomePage(module: module, baseURL: baseURL)
pages["Home"] = HomePage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
}

try pages.map { $0 }.parallelForEach {
Expand Down
16 changes: 16 additions & 0 deletions Sources/swift-doc/Supporting Types/AccessLevel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ArgumentParser
import SwiftDoc

enum AccessLevel: String, ExpressibleByArgument {
case `public`
case `internal`

func includes(symbol: Symbol) -> Bool {
switch self {
case .public:
return symbol.isPublic
case .internal:
return symbol.isPublic || symbol.isInternal
}
}
}
4 changes: 2 additions & 2 deletions Sources/swift-doc/Supporting Types/Components/Members.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ struct Members: Component {
var methods: [Symbol]
var genericallyConstrainedMembers: [[GenericRequirement] : [Symbol]]

init(of symbol: Symbol, in module: Module, baseURL: String) {
init(of symbol: Symbol, in module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
self.symbol = symbol
self.module = module
self.baseURL = baseURL

self.members = module.interface.members(of: symbol)
.filter { $0.extension?.genericRequirements.isEmpty != false }
.filter { $0.isPublic }
.filter(symbolFilter)

self.typealiases = members.filter { $0.api is Typealias }
self.initializers = members.filter { $0.api is Initializer }
Expand Down
4 changes: 2 additions & 2 deletions Sources/swift-doc/Supporting Types/Pages/HomePage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ struct HomePage: Page {
var globalFunctions: [Symbol] = []
var globalVariables: [Symbol] = []

init(module: Module, baseURL: String) {
init(module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
self.module = module
self.baseURL = baseURL

for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) {
for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
switch symbol.api {
case is Class:
classes.append(symbol)
Expand Down
4 changes: 2 additions & 2 deletions Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ struct SidebarPage: Page {
var globalFunctionNames: Set<String> = []
var globalVariableNames: Set<String> = []

init(module: Module, baseURL: String) {
init(module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
self.module = module
self.baseURL = baseURL

for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) {
for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
switch symbol.api {
case is Class:
typeNames.insert(symbol.id.description)
Expand Down
8 changes: 5 additions & 3 deletions Sources/swift-doc/Supporting Types/Pages/TypePage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ struct TypePage: Page {
let module: Module
let symbol: Symbol
let baseURL: String
let symbolFilter: (Symbol) -> Bool

init(module: Module, symbol: Symbol, baseURL: String) {
init(module: Module, symbol: Symbol, baseURL: String, includingChildren symbolFilter: @escaping (Symbol) -> Bool) {
precondition(symbol.api is Type)
self.module = module
self.symbol = symbol
self.baseURL = baseURL
self.symbolFilter = symbolFilter
}

// MARK: - Page
Expand All @@ -27,7 +29,7 @@ struct TypePage: Page {

Documentation(for: symbol, in: module, baseURL: baseURL)
Relationships(of: symbol, in: module, baseURL: baseURL)
Members(of: symbol, in: module, baseURL: baseURL)
Members(of: symbol, in: module, baseURL: baseURL, symbolFilter: symbolFilter)
Requirements(of: symbol, in: module, baseURL: baseURL)
}
}
Expand All @@ -41,7 +43,7 @@ struct TypePage: Page {

\#(Documentation(for: symbol, in: module, baseURL: baseURL).html)
\#(Relationships(of: symbol, in: module, baseURL: baseURL).html)
\#(Members(of: symbol, in: module, baseURL: baseURL).html)
\#(Members(of: symbol, in: module, baseURL: baseURL, symbolFilter: symbolFilter).html)
\#(Requirements(of: symbol, in: module, baseURL: baseURL).html)
"""#
}
Expand Down
Loading