add configuration for missing_docs (#3701)

* `excludes_extensions` defaults to `true` to skip reporting violations
  for extensions with missing documentation comments.
* `excludes_inherited_types` defaults to `true` to skip reporting
  violations for inherited declarations, like subclass overrides.
This commit is contained in:
Ben Fox
2021-09-01 14:42:47 -07:00
committed by GitHub
parent 6953fe268e
commit 1f44d56357
4 changed files with 109 additions and 26 deletions
+7
View File
@@ -11,6 +11,13 @@
#### Enhancements
* Add configuration options to `missing_docs` rule:
* `excludes_extensions` defaults to `true` to skip reporting violations
for extensions with missing documentation comments.
* `excludes_inherited_types` defaults to `true` to skip reporting
violations for inherited declarations, like subclass overrides.
[Ben Fox](https://github.com/bdfox325)
* Fix false negative on `redundant_optional_initialization` rule when variable
has observers.
[Isaac Ressler](https://github.com/iressler)
@@ -2,18 +2,25 @@ import SourceKittenFramework
private extension SwiftLintFile {
func missingDocOffsets(in dictionary: SourceKittenDictionary,
acls: [AccessControlLevel]) -> [(ByteCount, AccessControlLevel)] {
acls: [AccessControlLevel],
excludesExtensions: Bool,
excludesInheritedTypes: Bool) -> [(ByteCount, AccessControlLevel)] {
if dictionary.enclosedSwiftAttributes.contains(.override) ||
dictionary.inheritedTypes.isNotEmpty {
(dictionary.inheritedTypes.isNotEmpty && excludesInheritedTypes) {
return []
}
let substructureOffsets = dictionary.substructure.flatMap {
missingDocOffsets(in: $0, acls: acls)
missingDocOffsets(
in: $0,
acls: acls,
excludesExtensions: excludesExtensions,
excludesInheritedTypes: excludesInheritedTypes
)
}
let extensionKinds: Set<SwiftDeclarationKind> = [.extension, .extensionEnum, .extensionClass,
.extensionStruct, .extensionProtocol]
guard let kind = dictionary.declarationKind,
!extensionKinds.contains(kind),
(!extensionKinds.contains(kind) || !excludesExtensions),
case let isDeinit = kind == .functionMethodInstance && dictionary.name == "deinit",
!isDeinit,
let offset = dictionary.offset,
@@ -32,7 +39,9 @@ public struct MissingDocsRule: OptInRule, ConfigurationProviderRule, AutomaticTe
public init() {
configuration = MissingDocsRuleConfiguration(
parameters: [RuleParameter<AccessControlLevel>(severity: .warning, value: .open),
RuleParameter<AccessControlLevel>(severity: .warning, value: .public)])
RuleParameter<AccessControlLevel>(severity: .warning, value: .public)],
excludesExtensions: true,
excludesInheritedTypes: true)
}
public typealias ConfigurationType = MissingDocsRuleConfiguration
@@ -98,7 +107,12 @@ public struct MissingDocsRule: OptInRule, ConfigurationProviderRule, AutomaticTe
public func validate(file: SwiftLintFile) -> [StyleViolation] {
let acls = configuration.parameters.map { $0.value }
let dict = file.structureDictionary
return file.missingDocOffsets(in: dict, acls: acls).map { offset, acl in
return file.missingDocOffsets(
in: dict,
acls: acls,
excludesExtensions: configuration.excludesExtensions,
excludesInheritedTypes: configuration.excludesInheritedTypes
).map { offset, acl in
StyleViolation(ruleDescription: Self.description,
severity: configuration.parameters.first { $0.value == acl }?.severity ?? .warning,
location: Location(file: file, byteOffset: offset),
@@ -1,35 +1,70 @@
public struct MissingDocsRuleConfiguration: RuleConfiguration, Equatable {
private(set) var parameters = [RuleParameter<AccessControlLevel>]()
private(set) var excludesExtensions = true
private(set) var excludesInheritedTypes = true
public var consoleDescription: String {
return parameters.group { $0.severity }.sorted { $0.key.rawValue < $1.key.rawValue }.map {
let parametersDescription = parameters.group { $0.severity }.sorted { $0.key.rawValue < $1.key.rawValue }.map {
"\($0.rawValue): \($1.map { $0.value.description }.sorted(by: <).joined(separator: ", "))"
}.joined(separator: ", ")
if parametersDescription.isEmpty {
return [
"excludes_extensions: \(excludesExtensions)",
"excludes_inherited_types: \(excludesInheritedTypes)"
]
.joined(separator: ", ")
} else {
return [
parametersDescription,
"excludes_extensions: \(excludesExtensions)",
"excludes_inherited_types: \(excludesInheritedTypes)"
]
.joined(separator: ", ")
}
}
public mutating func apply(configuration: Any) throws {
guard let dict = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}
let parameters = try dict.flatMap { (key: String, value: Any) -> [RuleParameter<AccessControlLevel>] in
if let shouldExcludeExtensions = dict["excludes_extensions"] as? Bool {
excludesExtensions = shouldExcludeExtensions
}
if let shouldExcludeInheritedTypes = dict["excludes_inherited_types"] as? Bool {
excludesExtensions = shouldExcludeInheritedTypes
}
var parameters: [RuleParameter<AccessControlLevel>] = []
for (key, value) in dict {
guard let severity = ViolationSeverity(rawValue: key) else {
throw ConfigurationError.unknownConfiguration
}
if let array = [String].array(of: value) {
return try array.map {
guard let acl = AccessControlLevel(description: $0) else {
throw ConfigurationError.unknownConfiguration
let rules: [RuleParameter<AccessControlLevel>] = try array
.map { val -> RuleParameter<AccessControlLevel> in
guard let acl = AccessControlLevel(description: val) else {
throw ConfigurationError.unknownConfiguration
}
return RuleParameter<AccessControlLevel>(severity: severity, value: acl)
}
return RuleParameter<AccessControlLevel>(severity: severity, value: acl)
}
parameters.append(contentsOf: rules)
} else if let string = value as? String, let acl = AccessControlLevel(description: string) {
return [RuleParameter<AccessControlLevel>(severity: severity, value: acl)]
let rule = RuleParameter<AccessControlLevel>(severity: severity, value: acl)
parameters.append(rule)
}
throw ConfigurationError.unknownConfiguration
}
guard parameters.count == parameters.map({ $0.value }).unique.count else {
throw ConfigurationError.unknownConfiguration
}
self.parameters = parameters
}
}
@@ -4,49 +4,76 @@ import XCTest
class MissingDocsRuleConfigurationTests: XCTestCase {
func testDescriptionEmpty() {
let configuration = MissingDocsRuleConfiguration()
XCTAssertEqual(configuration.consoleDescription, "")
XCTAssertEqual(
configuration.consoleDescription,
"excludes_extensions: true, excludes_inherited_types: true"
)
}
func testDescriptionExcludesFalse() {
let configuration = MissingDocsRuleConfiguration(excludesExtensions: false, excludesInheritedTypes: false)
XCTAssertEqual(
configuration.consoleDescription,
"excludes_extensions: false, excludes_inherited_types: false"
)
}
func testDescriptionSingleServety() {
let configuration = MissingDocsRuleConfiguration(
parameters: [RuleParameter<AccessControlLevel>(severity: .error, value: .open)])
XCTAssertEqual(configuration.consoleDescription, "error: open")
XCTAssertEqual(
configuration.consoleDescription,
"error: open, excludes_extensions: true, excludes_inherited_types: true"
)
}
func testDescriptionMultipleSeverities() {
let configuration = MissingDocsRuleConfiguration(
parameters: [RuleParameter<AccessControlLevel>(severity: .error, value: .open),
RuleParameter<AccessControlLevel>(severity: .warning, value: .public)])
XCTAssertEqual(configuration.consoleDescription, "error: open, warning: public")
XCTAssertEqual(
configuration.consoleDescription,
"error: open, warning: public, excludes_extensions: true, excludes_inherited_types: true"
)
}
func testDescriptionMultipleAcls() {
let configuration = MissingDocsRuleConfiguration(
parameters: [RuleParameter<AccessControlLevel>(severity: .warning, value: .open),
RuleParameter<AccessControlLevel>(severity: .warning, value: .public)])
XCTAssertEqual(configuration.consoleDescription, "warning: open, public")
XCTAssertEqual(
configuration.consoleDescription,
"warning: open, public, excludes_extensions: true, excludes_inherited_types: true"
)
}
func testParsingSingleServety() {
var configuration = MissingDocsRuleConfiguration()
try? configuration.apply(configuration: ["warning": "open"])
XCTAssertEqual(configuration.parameters, [RuleParameter<AccessControlLevel>(severity: .warning, value: .open)])
XCTAssertEqual(
configuration.parameters,
[RuleParameter<AccessControlLevel>(severity: .warning, value: .open)]
)
}
func testParsingMultipleSeverities() {
var configuration = MissingDocsRuleConfiguration()
try? configuration.apply(configuration: ["warning": "public", "error": "open"])
XCTAssertEqual(configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue },
[RuleParameter<AccessControlLevel>(severity: .warning, value: .public),
RuleParameter<AccessControlLevel>(severity: .error, value: .open)])
XCTAssertEqual(
configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue },
[RuleParameter<AccessControlLevel>(severity: .warning, value: .public),
RuleParameter<AccessControlLevel>(severity: .error, value: .open)]
)
}
func testParsingMultipleAcls() {
var configuration = MissingDocsRuleConfiguration()
try? configuration.apply(configuration: ["warning": ["public", "open"]])
XCTAssertEqual(configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue },
[RuleParameter<AccessControlLevel>(severity: .warning, value: .public),
RuleParameter<AccessControlLevel>(severity: .warning, value: .open)])
XCTAssertEqual(
configuration.parameters.sorted { $0.value.rawValue > $1.value.rawValue },
[RuleParameter<AccessControlLevel>(severity: .warning, value: .public),
RuleParameter<AccessControlLevel>(severity: .warning, value: .open)]
)
}
func testInvalidServety() {