SwiftLint/Source/SwiftLintFramework/Rules/ValidDocsRule.swift

185 lines
9.3 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ValidDocsRule.swift
// SwiftLint
//
// Created by JP Simard on 2015-11-21.
// Copyright © 2015 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
extension File {
private func invalidDocOffsets(dictionary: [String: SourceKitRepresentable]) -> [Int] {
let substructure = (dictionary["key.substructure"] as? [SourceKitRepresentable])?
.flatMap { $0 as? [String: SourceKitRepresentable] } ?? []
let substructureOffsets = substructure.flatMap(invalidDocOffsets)
guard let kind = (dictionary["key.kind"] as? String).flatMap(SwiftDeclarationKind.init)
where kind != .VarParameter,
let offset = dictionary["key.offset"] as? Int64,
bodyOffset = dictionary["key.bodyoffset"] as? Int64,
comment = getDocumentationCommentBody(dictionary, syntaxMap: syntaxMap)
where !comment.containsString(":nodoc:") else {
return substructureOffsets
}
let declaration = (contents as NSString)
.substringWithByteRange(start: Int(offset), length: Int(bodyOffset - offset))!
let hasViolation = missingReturnDocumentation(declaration, comment: comment) ||
superfluousReturnDocumentation(declaration, comment: comment, kind: kind) ||
superfluousOrMissingThrowsDocumentation(declaration, comment: comment) ||
superfluousOrMissingParameterDocumentation(declaration, substructure: substructure,
offset: offset, bodyOffset: bodyOffset,
comment: comment)
return substructureOffsets + (hasViolation ? [Int(offset)] : [])
}
}
func superfluousOrMissingThrowsDocumentation(declaration: String, comment: String) -> Bool {
return declaration.containsString(" throws ") == !comment.containsString("- throws:")
}
func delcarationReturns(declaration: String, kind: SwiftDeclarationKind) -> Bool {
if SwiftDeclarationKind.variableKinds().contains(kind) { return true }
let outsideBraces = NSMutableString(string: declaration)
regex("(\\s*->\\s*)[^(]*\\)").replaceMatchesInString(outsideBraces, options: [],
range: NSRange(location: 0, length: outsideBraces.length), withTemplate: "")
return outsideBraces.containsString("->")
}
func commentHasBatchedParameters(comment: String) -> Bool {
return comment.lowercaseString.containsString("- parameters:")
}
func commentReturns(comment: String) -> Bool {
return comment.containsString("- returns:") ||
comment.rangeOfString("Returns")?.startIndex == comment.startIndex
}
func missingReturnDocumentation(declaration: String, comment: String) -> Bool {
let outsideBraces = NSMutableString(string: declaration)
regex("(\\s*->\\s*)[^(]*\\)").replaceMatchesInString(outsideBraces, options: [],
range: NSRange(location: 0, length: outsideBraces.length), withTemplate: "")
return outsideBraces.containsString("->") && !commentReturns(comment)
}
func superfluousReturnDocumentation(declaration: String, comment: String,
kind: SwiftDeclarationKind) -> Bool {
return !delcarationReturns(declaration, kind: kind) && commentReturns(comment)
}
func superfluousOrMissingParameterDocumentation(declaration: String,
substructure: [[String: SourceKitRepresentable]],
offset: Int64, bodyOffset: Int64,
comment: String) -> Bool {
// This function doesn't handle batched parameters, so skip those.
if commentHasBatchedParameters(comment) { return false }
let parameterNames = substructure.filter {
($0["key.kind"] as? String).flatMap(SwiftDeclarationKind.init) == .VarParameter
}.filter { subDict in
return (subDict["key.offset"] as? Int64).map({ $0 < bodyOffset }) ?? false
}.flatMap {
$0["key.name"] as? String
} ?? []
let labelsAndParams = parameterNames.map { parameter -> (label: String, parameter: String) in
let fullRange = NSRange(location: 0, length: declaration.utf16.count)
let firstMatch = regex("([^,\\s(]+)\\s+\(parameter)\\s*:")
.firstMatchInString(declaration, options: [], range: fullRange)
if let match = firstMatch {
let label = (declaration as NSString).substringWithRange(match.rangeAtIndex(1))
return (label, parameter)
}
return (parameter, parameter)
}
let optionallyDocumentedParameterCount = labelsAndParams.filter({ $0.0 == "_" }).count
let commentRange = NSRange(location: 0, length: comment.utf16.count)
let commentParameterMatches = regex("- parameter ([^:]+)")
.matchesInString(comment, options: [], range: commentRange)
let commentParameters = commentParameterMatches.map { match in
return (comment as NSString).substringWithRange(match.rangeAtIndex(1))
}
if commentParameters.count > labelsAndParams.count ||
labelsAndParams.count - commentParameters.count > optionallyDocumentedParameterCount {
return true
}
return !zip(commentParameters, labelsAndParams).filter {
![$1.label, $1.parameter].contains($0)
}.isEmpty
}
public struct ValidDocsRule: ConfigProviderRule {
public var config = SeverityConfig(.Warning)
public init() {}
public static let description = RuleDescription(
identifier: "valid_docs",
name: "Valid Docs",
description: "Documented declarations should be valid.",
nonTriggeringExamples: [
Trigger("/// docs\npublic func a() {}\n"),
Trigger("/// docs\n/// - parameter param: this is void\n" +
"public func a(param: Void) {}\n"),
Trigger("/// docs\n/// - parameter label: this is void\n" +
"public func a(label param: Void) {}"),
Trigger("/// docs\n/// - parameter param: this is void\n" +
"public func a(label param: Void) {}"),
Trigger("/// docs\n/// - returns: false\npublic func no() -> Bool { return false }"),
Trigger("/// Returns false\npublic func no() -> Bool { return false }"),
Trigger("/// Returns false\nvar no: Bool { return false }"),
Trigger("/// docs\nvar no: Bool { return false }"),
Trigger("/// docs\n/// - throws: NSError\nfunc a() throws {}"),
Trigger("/// docs\n/// - parameter param: this is void\n/// - returns: false" +
"\npublic func no(param: (Void -> Void)?) -> Bool { return false }"),
Trigger("/// docs\n/// - parameter param: this is void" +
"\n///- parameter param2: this is void too\n/// - returns: false"),
Trigger("\npublic func no(param: (Void -> Void)?, param2: String->Void) -> Bool" +
" {return false}"),
Trigger("/// docs\n/// - parameter param: this is void" +
"\npublic func no(param: (Void -> Void)?) {}"),
Trigger("/// docs\n/// - parameter param: this is void" +
"\n///- parameter param2: this is void too" +
"\npublic func no(param: (Void -> Void)?, param2: String->Void) {}"),
Trigger("/// docs👨👩👧👧\n/// - returns: false\n" +
"public func no() -> Bool { return false }"),
Trigger("/// docs\n" +
"/// - returns: tuple\npublic func no() -> (Int, Int) {return (1, 2)}"),
],
triggeringExamples: [
Trigger("/// docs\npublic ↓func a(param: Void) {}\n"),
Trigger("/// docs\n/// - parameter invalid: this is void\n" +
"public ↓func a(param: Void) {}"),
Trigger("/// docs\n/// - parameter invalid: this is void\n" +
"public ↓func a(label param: Void) {}"),
Trigger("/// docs\n/// - parameter invalid: this is void\npublic ↓func a() {}"),
Trigger("/// docs\npublic ↓func no() -> Bool { return false }"),
Trigger("/// Returns false\npublic ↓func a() {}"),
Trigger("/// docs\n/// - throws: NSError\n↓func a() {}"),
Trigger("/// docs\n↓func a() throws {}"),
Trigger("/// docs\n/// - parameter param: this is void" +
"\npublic ↓func no(param: (Void -> Void)?) -> Bool { return false }"),
Trigger("/// docs\n/// - parameter param: this is void" +
"\n///- parameter param2: this is void too" +
"\npublic ↓func no(param: (Void -> Void)?, param2: String->Void) -> " +
"Bool {return false}"),
Trigger("/// docs\n/// - parameter param: this is void\n/// - returns: false" +
"\npublic ↓func no(param: (Void -> Void)?) {}"),
Trigger("/// docs\n/// - parameter param: this is void" +
"\n///- parameter param2: this is void too\n/// - returns: false" +
"\npublic ↓func no(param: (Void -> Void)?, param2: String->Void) {}"),
]
)
public func validateFile(file: File) -> [StyleViolation] {
return file.invalidDocOffsets(file.structure.dictionary).map {
StyleViolation(ruleDescription: self.dynamicType.description,
severity: config.severity,
location: Location(file: file, byteOffset: $0))
}
}
}