From 37167a8a35700acdea5555afc585403c7aebfb6c Mon Sep 17 00:00:00 2001 From: JP Simard Date: Wed, 8 Jan 2020 09:47:10 -0800 Subject: [PATCH] Add documentation comments to all public declarations (#3027) --- .jazzy.yaml | 10 ++ .sourcery/MasterRuleList.stencil | 1 + .../Documentation/RuleDocumentation.swift | 2 +- .../Extensions/Configuration+Cache.swift | 2 + .../Configuration+IndentationStyle.swift | 7 ++ .../Configuration+LintableFiles.swift | 29 ++++- .../Extensions/Configuration+Merging.swift | 6 + .../Extensions/Configuration+Parsing.swift | 8 ++ .../Extensions/Dictionary+SwiftLint.swift | 14 +++ .../Extensions/FileManager+SwiftLint.swift | 20 ++- .../NSRegularExpression+SwiftLint.swift | 11 +- .../Extensions/QueuedPrint.swift | 6 +- .../Extensions/String+SwiftLint.swift | 1 + ...ftDeclarationAttributeKind+Swiftlint.swift | 4 +- .../Extensions/SwiftExpressionKind.swift | 8 ++ .../Extensions/SwiftLintFile+Cache.swift | 1 + .../Helpers/RegexHelpers.swift | 12 +- .../Models/AccessControlLevel.swift | 16 ++- .../SwiftLintFramework/Models/Command.swift | 29 ++++- .../Models/Configuration.swift | 35 ++++-- .../Models/ConfigurationError.swift | 2 + .../Models/Correction.swift | 4 + Source/SwiftLintFramework/Models/Linter.swift | 16 +++ .../Models/LinterCache.swift | 6 + .../SwiftLintFramework/Models/Location.swift | 23 ++++ .../Models/MasterRuleList.swift | 1 + Source/SwiftLintFramework/Models/Region.swift | 24 ++++ .../Models/RuleDescription.swift | 44 +++++++ .../Models/RuleIdentifier.swift | 18 +++ .../SwiftLintFramework/Models/RuleKind.swift | 6 + .../SwiftLintFramework/Models/RuleList.swift | 14 +++ .../Models/RuleParameter.swift | 7 ++ .../Models/RuleStorage.swift | 10 ++ .../Models/StyleViolation.swift | 16 +++ .../Models/SwiftLintFile.swift | 25 +++- .../Models/SwiftLintSyntaxMap.swift | 14 ++- .../Models/SwiftLintSyntaxToken.swift | 10 ++ .../Models/SwiftVersion.swift | 15 ++- .../SwiftLintFramework/Models/Version.swift | 3 + .../Models/ViolationSeverity.swift | 3 + .../Models/YamlParser.swift | 5 + .../Protocols/ASTRule.swift | 17 +++ .../Protocols/Reporter.swift | 14 +++ .../SwiftLintFramework/Protocols/Rule.swift | 118 +++++++++++++++++- .../Protocols/RuleConfiguration.swift | 7 ++ .../Reporters/CSVReporter.swift | 27 ++-- .../Reporters/CheckstyleReporter.swift | 6 + .../Reporters/EmojiReporter.swift | 5 + .../GitHubActionsLoggingReporter.swift | 7 +- .../Reporters/HTMLReporter.swift | 9 +- .../Reporters/JSONReporter.swift | 7 +- .../Reporters/JUnitReporter.swift | 3 + .../Reporters/MarkdownReporter.swift | 31 +++-- .../Reporters/SonarQubeReporter.swift | 5 + .../Reporters/XcodeReporter.swift | 6 + .../ModifierOrderConfiguration.swift | 6 +- azure-pipelines.yml | 16 ++- 57 files changed, 693 insertions(+), 79 deletions(-) diff --git a/.jazzy.yaml b/.jazzy.yaml index 8f819b874..6eee0c1e0 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -18,3 +18,13 @@ custom_categories: - name: Rules children: - Rule Directory + - name: Reporters + children: + - CSVReporter + - CheckstyleReporter + - EmojiReporter + - GitHubActionsLoggingReporter + - HTMLReporter + - JSONReporter + - JUnitReporter + - MarkdownReporter diff --git a/.sourcery/MasterRuleList.stencil b/.sourcery/MasterRuleList.stencil index 0d0c41ee8..fcbe10ced 100644 --- a/.sourcery/MasterRuleList.stencil +++ b/.sourcery/MasterRuleList.stencil @@ -1,3 +1,4 @@ +/// The rule list containing all available rules built into SwiftLint. public let masterRuleList = RuleList(rules: [ {% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %} {% endfor %}]) diff --git a/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift b/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift index 021342dd5..b10671ae2 100644 --- a/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift +++ b/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift @@ -1,5 +1,5 @@ /// User-facing documentation for a SwiftLint rule. -public struct RuleDocumentation { +struct RuleDocumentation { private let ruleType: Rule.Type /// Creates a RuleDocumentation instance from a Rule type. diff --git a/Source/SwiftLintFramework/Extensions/Configuration+Cache.swift b/Source/SwiftLintFramework/Extensions/Configuration+Cache.swift index 7b6bf4a6f..677a03218 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+Cache.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+Cache.swift @@ -37,6 +37,8 @@ extension Configuration { return cachedConfigurationsByPath[path] } + /// Returns a copy of the current `Configuration` with its `computedCacheDescription` property set to the value of + /// `cacheDescription`, which is expensive to compute. public func withPrecomputedCacheDescription() -> Configuration { var result = self result.computedCacheDescription = result.cacheDescription diff --git a/Source/SwiftLintFramework/Extensions/Configuration+IndentationStyle.swift b/Source/SwiftLintFramework/Extensions/Configuration+IndentationStyle.swift index 3402f9c16..f21184fca 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+IndentationStyle.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+IndentationStyle.swift @@ -1,10 +1,17 @@ public extension Configuration { + /// The style of indentation used in a Swift project. enum IndentationStyle: Equatable { + /// Swift source code should be indented using tabs. case tabs + /// Swift source code should be indented using spaces with `count` spaces per indentation level. case spaces(count: Int) + /// The default indentation style if none is explicitly provided. public static var `default` = spaces(count: 4) + /// Creates an indentation style based on an untyped configuration value. + /// + /// - parameter object: The configuration value. internal init?(_ object: Any?) { switch object { case let value as Int: self = .spaces(count: value) diff --git a/Source/SwiftLintFramework/Extensions/Configuration+LintableFiles.swift b/Source/SwiftLintFramework/Extensions/Configuration+LintableFiles.swift index 5e01b15a3..7dd38b1d7 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+LintableFiles.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+LintableFiles.swift @@ -1,11 +1,26 @@ import Foundation extension Configuration { + /// Returns the files that can be linted by SwiftLint in the specified parent path. + /// + /// - parameter path: The parent path in which to search for lintable files. Can be a directory or a file. + /// - parameter forceExclude: Whether or not excludes defined in this configuration should be applied even if `path` + /// is an exact match. + /// + /// - returns: Files to lint. public func lintableFiles(inPath path: String, forceExclude: Bool) -> [SwiftLintFile] { return lintablePaths(inPath: path, forceExclude: forceExclude) .compactMap(SwiftLintFile.init(pathDeferringReading:)) } + /// Returns the paths for files that can be linted by SwiftLint in the specified parent path. + /// + /// - parameter path: The parent path in which to search for lintable files. Can be a directory or a file. + /// - parameter forceExclude: Whether or not excludes defined in this configuration should be applied even if `path` + /// is an exact match. + /// - parameter fileManager: The lintable file manager to use to search for lintable files. + /// + /// - returns: Paths for files to lint. internal func lintablePaths(inPath path: String, forceExclude: Bool, fileManager: LintableFileManager = FileManager.default) -> [String] { // If path is a file and we're not forcing excludes, skip filtering with excluded/included paths @@ -18,9 +33,13 @@ extension Configuration { } return filterExcludedPaths(fileManager: fileManager, in: pathsForPath, includedPaths) } -} -extension Configuration { + /// Returns an array of file paths after removing the excluded paths as defined by this configuration. + /// + /// - parameter fileManager: The lintable file manager to use to expand the excluded paths into all matching paths. + /// - parameter paths: The input paths to filter. + /// + /// - returns: The input paths after removing the excluded paths. public func filterExcludedPaths(fileManager: LintableFileManager = FileManager.default, in paths: [String]...) -> [String] { let allPaths = paths.flatMap { $0 } @@ -36,6 +55,12 @@ extension Configuration { return result.map { $0 as! String } } + /// Returns the file paths that are excluded by this configuration after expanding them using the specified file + /// manager. + /// + /// - parameter fileManager: The file manager to get child paths in a given parent location. + /// + /// - returns: The expanded excluded file paths. internal func excludedPaths(fileManager: LintableFileManager) -> [String] { return excluded .flatMap(Glob.resolveGlob) diff --git a/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift b/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift index d9a29d9cf..6510e416f 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+Merging.swift @@ -2,6 +2,12 @@ import Foundation import SourceKittenFramework extension Configuration { + /// Returns a new configuration that applies to the specified file by merging the current configuration with any + /// child configurations in the directory inheritance graph present until the level of the specified file. + /// + /// - parameter file: The file for which to obtain a configuration value. + /// + /// - returns: A new configuration. public func configuration(for file: SwiftLintFile) -> Configuration { if let containingDir = file.path?.bridge().deletingLastPathComponent { return configuration(forPath: containingDir) diff --git a/Source/SwiftLintFramework/Extensions/Configuration+Parsing.swift b/Source/SwiftLintFramework/Extensions/Configuration+Parsing.swift index 543a6ff0f..f32e9bc8c 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+Parsing.swift @@ -52,6 +52,14 @@ extension Configuration { return .default } + /// Creates a Configuration value based on the specified parameters. + /// + /// - parameter dict: The untyped dictionary to serve as the input for this typed configuration. + /// Typically generated from a YAML-formatted file. + /// - parameter ruleList: The list of rules to be available to this configuration. + /// - parameter enableAllRules: Whether all rules from `ruleList` should be enabled, regardless of the + /// settings in `dict`. + /// - parameter cachePath: The location of the persisted cache on disk. public init?(dict: [String: Any], ruleList: RuleList = masterRuleList, enableAllRules: Bool = false, cachePath: String? = nil, customRulesIdentifiers: [String] = []) { // Use either new 'opt_in_rules' or deprecated 'enabled_rules' for now. diff --git a/Source/SwiftLintFramework/Extensions/Dictionary+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/Dictionary+SwiftLint.swift index 63e07eac7..a8b711cfd 100644 --- a/Source/SwiftLintFramework/Extensions/Dictionary+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/Dictionary+SwiftLint.swift @@ -1,16 +1,27 @@ import Foundation import SourceKittenFramework +/// A collection of keys and values as parsed out of SourceKit, with many conveniences for accessing SwiftLint-specific +/// values. public struct SourceKittenDictionary { + /// The underlying SourceKitten dictionary. public let value: [String: SourceKitRepresentable] + /// The cached substructure for this dictionary. Empty if there is no substructure. public let substructure: [SourceKittenDictionary] + /// The kind of Swift expression represented by this dictionary, if it is an expression. public let expressionKind: SwiftExpressionKind? + /// The kind of Swift declaration represented by this dictionary, if it is a declaration. public let declarationKind: SwiftDeclarationKind? + /// The kind of Swift statement represented by this dictionary, if it is a statement. public let statementKind: StatementKind? + /// The accessibility level for this dictionary, if it is a declaration. public let accessibility: AccessControlLevel? + /// Creates a SourceKitten dictionary given a `Dictionary` input. + /// + /// - parameter value: The input dictionary/ init(_ value: [String: SourceKitRepresentable]) { self.value = value @@ -91,15 +102,18 @@ public struct SourceKittenDictionary { return (value["key.doclength"] as? Int64).flatMap({ Int($0) }) } + /// The attribute for this dictionary, as returned by SourceKit. var attribute: String? { return value["key.attribute"] as? String } + /// The `SwiftDeclarationAttributeKind` values associated with this dictionary. var enclosedSwiftAttributes: [SwiftDeclarationAttributeKind] { return swiftAttributes.compactMap { $0.attribute } .compactMap(SwiftDeclarationAttributeKind.init(rawValue:)) } + /// The fully preserved SourceKitten dictionaries for all the attributes associated with this dictionary. var swiftAttributes: [SourceKittenDictionary] { let array = value["key.attributes"] as? [SourceKitRepresentable] ?? [] let dictionaries = array.compactMap { $0 as? [String: SourceKitRepresentable] } diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index b69149813..f8cd41d55 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -1,8 +1,24 @@ import Foundation +/// An interface for enumerating files that can be linted by SwiftLint. public protocol LintableFileManager { - func filesToLint(inPath: String, rootDirectory: String?) -> [String] - func modificationDate(forFileAtPath: String) -> Date? + /// Returns all files that can be linted in the specified path. If the path is relative, it will be appended to the + /// specified root path, or currentt working directory if no root directory is specified. + /// + /// - parameter path: The path in which lintable files should be found. + /// - parameter rootDirectory: The parent directory for the specified path. If none is provided, the current working + /// directory will be used. + /// + /// - returns: Files to lint. + func filesToLint(inPath path: String, rootDirectory: String?) -> [String] + + /// Returns the date when the file at the specified path was last modified. Returns `nil` if the file cannot be + /// found or its last modification date cannot be determined. + /// + /// - parameter path: The file whose modification date should be determined. + /// + /// - returns: A date, if one was determined. + func modificationDate(forFileAtPath path: String) -> Date? } extension FileManager: LintableFileManager { diff --git a/Source/SwiftLintFramework/Extensions/NSRegularExpression+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/NSRegularExpression+SwiftLint.swift index 21fc15028..4fb73efb8 100644 --- a/Source/SwiftLintFramework/Extensions/NSRegularExpression+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/NSRegularExpression+SwiftLint.swift @@ -5,17 +5,12 @@ private var regexCache = [RegexCacheKey: NSRegularExpression]() private let regexCacheLock = NSLock() private struct RegexCacheKey: Hashable { - // Disable unused private declaration rule here because even though we don't use these properties - // directly, we rely on them for their hashable and equatable behavior. - // swiftlint:disable unused_declaration let pattern: String let options: NSRegularExpression.Options - // swiftlint:enable unused_declaration -} -extension NSRegularExpression.Options: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(rawValue) + func hash(into hasher: inout Hasher) { + hasher.combine(pattern) + hasher.combine(options.rawValue) } } diff --git a/Source/SwiftLintFramework/Extensions/QueuedPrint.swift b/Source/SwiftLintFramework/Extensions/QueuedPrint.swift index b2517ff3c..ee1cf5756 100644 --- a/Source/SwiftLintFramework/Extensions/QueuedPrint.swift +++ b/Source/SwiftLintFramework/Extensions/QueuedPrint.swift @@ -18,7 +18,7 @@ private let outputQueue: DispatchQueue = { }() /** - A thread-safe version of Swift's standard print(). + A thread-safe version of Swift's standard `print()`. - parameter object: Object to print. */ @@ -29,7 +29,7 @@ public func queuedPrint(_ object: T) { } /** - A thread-safe, newline-terminated version of fputs(..., stderr). + A thread-safe, newline-terminated version of `fputs(..., stderr)`. - parameter string: String to print. */ @@ -41,7 +41,7 @@ public func queuedPrintError(_ string: String) { } /** - A thread-safe, newline-terminated version of fatalError that doesn't leak + A thread-safe, newline-terminated version of `fatalError(...)` that doesn't leak the source path from the compiled binary. */ public func queuedFatalError(_ string: String, file: StaticString = #file, line: UInt = #line) -> Never { diff --git a/Source/SwiftLintFramework/Extensions/String+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/String+SwiftLint.swift index 75a2aa665..164220c61 100644 --- a/Source/SwiftLintFramework/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/String+SwiftLint.swift @@ -74,6 +74,7 @@ extension String { return NSRange(location: 0, length: utf16.count) } + /// Returns a new string, converting the path to a canonical absolute path. public func absolutePathStandardized() -> String { return bridge().absolutePathRepresentation().bridge().standardizingPath } diff --git a/Source/SwiftLintFramework/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift b/Source/SwiftLintFramework/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift index 1ef8a54ae..89cd9bd1f 100644 --- a/Source/SwiftLintFramework/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift +++ b/Source/SwiftLintFramework/Extensions/SwiftDeclarationAttributeKind+Swiftlint.swift @@ -1,6 +1,6 @@ import SourceKittenFramework -public extension SwiftDeclarationAttributeKind { +extension SwiftDeclarationAttributeKind { enum ModifierGroup: String, CustomDebugStringConvertible { case `override` case acl @@ -85,7 +85,7 @@ public extension SwiftDeclarationAttributeKind { } } - public var debugDescription: String { + var debugDescription: String { return self.rawValue } } diff --git a/Source/SwiftLintFramework/Extensions/SwiftExpressionKind.swift b/Source/SwiftLintFramework/Extensions/SwiftExpressionKind.swift index a34540dc7..dbd4b3219 100644 --- a/Source/SwiftLintFramework/Extensions/SwiftExpressionKind.swift +++ b/Source/SwiftLintFramework/Extensions/SwiftExpressionKind.swift @@ -1,9 +1,17 @@ +/// The kind of expression for a contiguous set of Swift source tokens. public enum SwiftExpressionKind: String { + /// A call to a named function or closure. case call = "source.lang.swift.expr.call" + /// An argument value for a function or closure. case argument = "source.lang.swift.expr.argument" + /// An Array expression. case array = "source.lang.swift.expr.array" + /// A Dictionary expression. case dictionary = "source.lang.swift.expr.dictionary" + /// An object literal expression. https://developer.apple.com/swift/blog/?id=33 case objectLiteral = "source.lang.swift.expr.object_literal" + /// A closure expression. https://docs.swift.org/swift-book/LanguageGuide/Closures.html case closure = "source.lang.swift.expr.closure" + /// A tuple expression. https://docs.swift.org/swift-book/ReferenceManual/Types.html#ID448 case tuple = "source.lang.swift.expr.tuple" } diff --git a/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift b/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift index e0c428eb3..36b7d609e 100644 --- a/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift +++ b/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift @@ -180,6 +180,7 @@ extension SwiftLintFile { return syntaxKindsByLines } + /// Invalidates all cached data for this file. public func invalidateCache() { responseCache.invalidate(self) assertHandlerCache.invalidate(self) diff --git a/Source/SwiftLintFramework/Helpers/RegexHelpers.swift b/Source/SwiftLintFramework/Helpers/RegexHelpers.swift index 807083b71..eccc249db 100644 --- a/Source/SwiftLintFramework/Helpers/RegexHelpers.swift +++ b/Source/SwiftLintFramework/Helpers/RegexHelpers.swift @@ -1,19 +1,19 @@ struct RegexHelpers { - // A single variable + /// A single variable static let varName = "[a-zA-Z_][a-zA-Z0-9_]+" - // A single variable in a group (capturable) + /// A single variable in a group (capturable) static let varNameGroup = "\\s*(\(varName))\\s*" - // Two variables (capturables) + /// Two variables (capturables) static let twoVars = "\(varNameGroup),\(varNameGroup)" - // A number + /// A number static let number = "[\\-0-9\\.]+" - // A variable or a number (capturable) + /// A variable or a number (capturable) static let variableOrNumber = "\\s*(\(varName)|\(number))\\s*" - // Two 'variable or number' + /// Two 'variable or number' static let twoVariableOrNumber = "\(variableOrNumber),\(variableOrNumber)" } diff --git a/Source/SwiftLintFramework/Models/AccessControlLevel.swift b/Source/SwiftLintFramework/Models/AccessControlLevel.swift index 3694a0ab8..b145dcd16 100644 --- a/Source/SwiftLintFramework/Models/AccessControlLevel.swift +++ b/Source/SwiftLintFramework/Models/AccessControlLevel.swift @@ -1,10 +1,21 @@ +/// The accessibility of a Swift source declaration. +/// +/// - SeeAlso: https://github.com/apple/swift/blob/master/docs/AccessControl.rst public enum AccessControlLevel: String, CustomStringConvertible { + /// Accessible by the declaration's immediate lexical scope. case `private` = "source.lang.swift.accessibility.private" + /// Accessible by the declaration's same file. case `fileprivate` = "source.lang.swift.accessibility.fileprivate" + /// Accessible by the declaration's same module, or modules importing it with the `@testable` attribute. case `internal` = "source.lang.swift.accessibility.internal" + /// Accessible by the declaration's same program. case `public` = "source.lang.swift.accessibility.public" + /// Accessible and customizable (via subclassing or overrides) by the declaration's same program. case `open` = "source.lang.swift.accessibility.open" + /// Initializes an access control level by its Swift source keyword value. + /// + /// - parameter value: The value used to describe this level in Swift source code. internal init?(description value: String) { switch value { case "private": self = .private @@ -16,7 +27,10 @@ public enum AccessControlLevel: String, CustomStringConvertible { } } - init?(identifier value: String) { + /// Initializes an access control level by its SourceKit unique identifier. + /// + /// - parameter value: The value used by SourceKit to refer to this access control level. + internal init?(identifier value: String) { self.init(rawValue: value) } diff --git a/Source/SwiftLintFramework/Models/Command.swift b/Source/SwiftLintFramework/Models/Command.swift index b33d64e69..887dc8018 100644 --- a/Source/SwiftLintFramework/Models/Command.swift +++ b/Source/SwiftLintFramework/Models/Command.swift @@ -29,11 +29,17 @@ private extension Scanner { } #endif +/// A SwiftLint-interpretable command to modify SwiftLint's behavior embedded as comments in source code. public struct Command: Equatable { + /// The action (verb) that SwiftLint should perform when interpreting this command. public enum Action: String { + /// The rule(s) associated with this command should be enabled by the SwiftLint engine. case enable + /// The rule(s) associated with this command should be disabled by the SwiftLint engine. case disable + /// Returns the inverse action that can cancel out the current action, restoring the SwifttLint engine's state + /// prior to the current action. internal func inverse() -> Action { switch self { case .enable: return .disable @@ -42,14 +48,18 @@ public struct Command: Equatable { } } + /// The modifier for a command, used to modify its scope. public enum Modifier: String { + /// The command should only apply to the line preceding its definition. case previous + /// The command should only apply to the same line as its definition. case this + /// The command should only apply to the line following its definition. case next } /// Text after this delimiter is not considered part of the rule. - /// The purpose of this delimiter is to allow swiftlint + /// The purpose of this delimiter is to allow SwiftLint /// commands to be documented in source code. /// /// swiftlint:disable:next force_try - Explanation here @@ -63,6 +73,15 @@ public struct Command: Equatable { /// Currently unused but parsed separate from rule identifiers internal let trailingComment: String? + /// Creates a command based on the specified parameters. + /// + /// - parameter action: This command's action. + /// - parameter ruleIdentifiers: The identifiers for the rules associated with this command. + /// - parameter line: The line in the source file where this command is defined. + /// - parameter character: The character offset within the line in the source file where this command is + /// defined. + /// - parameter modifier: This command's modifier, if any. + /// - parameter trailingComment: The comment following this command's `-` delimeter, if any. public init(action: Action, ruleIdentifiers: Set, line: Int = 0, character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) { self.action = action @@ -73,6 +92,12 @@ public struct Command: Equatable { self.trailingComment = trailingComment } + /// Creates a command based on the specified parameters. + /// + /// - parameter actionString: The string in the command's definition describing its action. + /// - parameter line: The line in the source file where this command is defined. + /// - parameter character: The character offset within the line in the source file where this command is + /// defined. public init?(actionString: String, line: Int, character: Int) { let scanner = Scanner(string: actionString) _ = scanner.scanString(string: "swiftlint:") @@ -117,6 +142,8 @@ public struct Command: Equatable { } } + /// Expands the current command into its fully descriptive form without any modifiers. + /// If the command doesn't have a modifier, it is returned as-is. internal func expand() -> [Command] { guard let modifier = modifier else { return [self] diff --git a/Source/SwiftLintFramework/Models/Configuration.swift b/Source/SwiftLintFramework/Models/Configuration.swift index 5a0d3c659..53102ac6d 100644 --- a/Source/SwiftLintFramework/Models/Configuration.swift +++ b/Source/SwiftLintFramework/Models/Configuration.swift @@ -1,25 +1,40 @@ import Foundation import SourceKittenFramework +/// The configuration struct for SwiftLint. User-defined in the `.swiftlint.yml` file, drives the behavior of SwiftLint. public struct Configuration: Hashable { - // Represents how a Configuration object can be configured with regards to rules. + /// Represents how a Configuration object can be configured with regards to rules. public enum RulesMode { + /// The default rules mode, which will enable all rules that aren't defined as being opt-in + /// (conforming to the `OptInRule` protocol), minus the rules listed in `disabled`, plus the rules lised in + /// `optIn`. case `default`(disabled: [String], optIn: [String]) + /// Only enable the rules explicitly listed. case whitelisted([String]) + /// Enable all available rules. case allEnabled } // MARK: Properties + /// The standard file name to look for user-defined configurations. public static let fileName = ".swiftlint.yml" - public let indentation: IndentationStyle // style to use when indenting - public let included: [String] // included - public let excluded: [String] // excluded - public let reporter: String // reporter (xcode, json, csv, checkstyle) - public let warningThreshold: Int? // warning threshold - public private(set) var rootPath: String? // the root path to search for nested configurations - public private(set) var configurationPath: String? // if successfully loaded from a path + /// The style to use when indenting Swift source code. + public let indentation: IndentationStyle + /// Included paths to lint. + public let included: [String] + /// Excluded paths to not lint. + public let excluded: [String] + /// The identifier for the `Reporter` to use to report style violations. + public let reporter: String + /// The threshold for the number of warnings to tolerate before treating the lint as having failed. + public let warningThreshold: Int? + /// The root directory to search for nested configurations. + public private(set) var rootPath: String? + /// The absolute path from where this configuration was loaded from, if any. + public private(set) var configurationPath: String? + /// The location of the persisted cache to use whith this configuration. public let cachePath: String? public func hash(into hasher: inout Hasher) { @@ -45,13 +60,14 @@ public struct Configuration: Hashable { // MARK: Rules Properties - // All rules enabled in this configuration, derived from disabled, opt-in and whitelist rules + /// All rules enabled in this configuration, derived from disabled, opt-in and whitelist rules public let rules: [Rule] internal let rulesMode: RulesMode // MARK: Initializers + /// Creates a `Configuration` by specifying its properties directly. public init?(rulesMode: RulesMode = .default(disabled: [], optIn: []), included: [String] = [], excluded: [String] = [], @@ -126,6 +142,7 @@ public struct Configuration: Hashable { indentation = configuration.indentation } + /// Creates a `Configuration` with convenience parameters. public init(path: String = Configuration.fileName, rootPath: String? = nil, optional: Bool = true, quiet: Bool = false, enableAllRules: Bool = false, cachePath: String? = nil, customRulesIdentifiers: [String] = []) { diff --git a/Source/SwiftLintFramework/Models/ConfigurationError.swift b/Source/SwiftLintFramework/Models/ConfigurationError.swift index 158eddf3c..01ade19ba 100644 --- a/Source/SwiftLintFramework/Models/ConfigurationError.swift +++ b/Source/SwiftLintFramework/Models/ConfigurationError.swift @@ -1,3 +1,5 @@ +/// All possible configuration errors. public enum ConfigurationError: Error { + /// The configuration didn't match internal expectations. case unknownConfiguration } diff --git a/Source/SwiftLintFramework/Models/Correction.swift b/Source/SwiftLintFramework/Models/Correction.swift index 6cc407e8d..71ff06a12 100644 --- a/Source/SwiftLintFramework/Models/Correction.swift +++ b/Source/SwiftLintFramework/Models/Correction.swift @@ -1,7 +1,11 @@ +/// A value describing a SwiftLint violation that was corrected. public struct Correction: Equatable { + /// The description of the rule for which this correction was applied. public let ruleDescription: RuleDescription + /// The location of the original violation that was corrected. public let location: Location + /// The console-printable description for this correction. public var consoleDescription: String { return "\(location) Corrected \(ruleDescription.name)" } diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index 14ccd292d..69e576697 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -110,13 +110,21 @@ private extension Rule { /// Represents a file that can be linted for style violations and corrections after being collected. public struct Linter { + /// The file to lint with this linter. public let file: SwiftLintFile + /// Whether or not this linter will be used to collect information from several files. public var isCollecting: Bool fileprivate let rules: [Rule] fileprivate let cache: LinterCache? fileprivate let configuration: Configuration fileprivate let compilerArguments: [String] + /// Creates a `Linter` by specifying its properties directly. + /// + /// - parameter file: The file to lint with this linter. + /// - parameter configuration: The SwiftLint configuration to apply to this linter. + /// - parameter cache: The persisted cache to use for this linter. + /// - parameter compilerArguments: The compiler arguments to use for this linter if it is to execute analyzer rules. public init(file: SwiftLintFile, configuration: Configuration = Configuration()!, cache: LinterCache? = nil, compilerArguments: [String] = []) { self.file = file @@ -147,6 +155,7 @@ public struct Linter { /// /// A `CollectedLinter` is only created after a `Linter` has run its collection steps in `Linter.collect(into:)`. public struct CollectedLinter { + /// The file to lint with this linter. public let file: SwiftLintFile private let rules: [Rule] private let cache: LinterCache? @@ -161,10 +170,12 @@ public struct CollectedLinter { compilerArguments = linter.compilerArguments } + /// Computes or retrieves style violations. public func styleViolations(using storage: RuleStorage) -> [StyleViolation] { return getStyleViolations(using: storage).0 } + /// Computes or retrieves style violations and the time spent executing each rule. public func styleViolationsAndRuleTimes(using storage: RuleStorage) -> ([StyleViolation], [(id: String, time: Double)]) { return getStyleViolations(using: storage, benchmark: true) @@ -234,6 +245,7 @@ public struct CollectedLinter { return (cachedViolations, ruleTimes) } + /// Applies corrections for all rules to this file, returning performed corrections. public func correct(using storage: RuleStorage) -> [Correction] { if let violations = cachedStyleViolations()?.0, violations.isEmpty { return [] @@ -250,6 +262,10 @@ public struct CollectedLinter { return corrections } + /// Formats the file associated with this linter. + /// + /// - parameter useTabs: Should the file be formatted using tabs? + /// - parameter indentWidth: How many spaces should be used per indentation level. public func format(useTabs: Bool, indentWidth: Int) { let formattedContents = try? file.file.format(trimmingTrailingWhitespace: true, useTabs: useTabs, diff --git a/Source/SwiftLintFramework/Models/LinterCache.swift b/Source/SwiftLintFramework/Models/LinterCache.swift index 94941e7a7..25533f7f6 100644 --- a/Source/SwiftLintFramework/Models/LinterCache.swift +++ b/Source/SwiftLintFramework/Models/LinterCache.swift @@ -16,6 +16,7 @@ private struct FileCache: Codable { static var empty: FileCache { return FileCache(entries: [:]) } } +/// A persisted cache for storing and retrieving linter results. public final class LinterCache { #if canImport(Darwin) || compiler(>=5.1.0) private typealias Encoder = PropertyListEncoder @@ -44,6 +45,10 @@ public final class LinterCache { self.swiftVersion = swiftVersion } + /// Creates a `LinterCache` by specifying a SwiftLint configuration and a file manager. + /// + /// - parameter configuration: The SwiftLint configuration for which this cache will be used. + /// - parameter fileManager: The file manager to use to read lintable file information. public init(configuration: Configuration, fileManager: LintableFileManager = FileManager.default) { location = configuration.cacheURL lazyReadCache = Cache() @@ -85,6 +90,7 @@ public final class LinterCache { return entry.violations } + /// Persists the cache to disk. public func save() throws { guard let url = location else { throw LinterCacheError.noLocation diff --git a/Source/SwiftLintFramework/Models/Location.swift b/Source/SwiftLintFramework/Models/Location.swift index 4f5128fc6..4a2021231 100644 --- a/Source/SwiftLintFramework/Models/Location.swift +++ b/Source/SwiftLintFramework/Models/Location.swift @@ -1,10 +1,16 @@ import Foundation import SourceKittenFramework +/// The placement of a segment of Swift in a collection of source files. public struct Location: CustomStringConvertible, Comparable, Codable { + /// The file path on disk for this location. public let file: String? + /// The line offset in the file for this location. 1-indexed. public let line: Int? + /// The character offset in the file for this location. 1-indexed. public let character: Int? + + /// A lossless printable description of this location. public var description: String { // Xcode likes warnings and errors in the following format: // {full_path_to_file}{:line}{:character}: {error,warning}: {content} @@ -13,16 +19,28 @@ public struct Location: CustomStringConvertible, Comparable, Codable { let charString: String = ":\(character ?? 1)" return [fileString, lineString, charString].joined() } + + /// The file path for this location relative to the current working directory. public var relativeFile: String? { return file?.replacingOccurrences(of: FileManager.default.currentDirectoryPath + "/", with: "") } + /// Creates a `Location` by specifying its properties directly. + /// + /// - parameter file: The file path on disk for this location. + /// - parameter line: The line offset in the file for this location. 1-indexed. + /// - parameter character: The character offset in the file for this location. 1-indexed. public init(file: String?, line: Int? = nil, character: Int? = nil) { self.file = file self.line = line self.character = character } + /// Creates a `Location` based on a `SwiftLintFile` and a byte-offset into the file. + /// Fails if tthe specified offset was not a valid location in the file. + /// + /// - parameter file: The file for this location. + /// - parameter offset: The offset in bytes into the file for this location. public init(file: SwiftLintFile, byteOffset offset: Int) { self.file = file.path if let lineAndCharacter = file.stringView.lineAndCharacter(forByteOffset: offset) { @@ -34,6 +52,11 @@ public struct Location: CustomStringConvertible, Comparable, Codable { } } + /// Creates a `Location` based on a `SwiftLintFile` and a UTF8 character-offset into the file. + /// Fails if tthe specified offset was not a valid location in the file. + /// + /// - parameter file: The file for this location. + /// - parameter offset: The offset in UTF8 fragments into the file for this location. public init(file: SwiftLintFile, characterOffset offset: Int) { self.file = file.path if let lineAndCharacter = file.stringView.lineAndCharacter(forCharacterOffset: offset) { diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 814f46c0f..8c8aca574 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -1,6 +1,7 @@ // Generated using Sourcery 0.17.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT +/// The rule list containing all available rules built into SwiftLint. public let masterRuleList = RuleList(rules: [ AnyObjectProtocolRule.self, ArrayInitRule.self, diff --git a/Source/SwiftLintFramework/Models/Region.swift b/Source/SwiftLintFramework/Models/Region.swift index a1cadb934..baf3d3ddb 100644 --- a/Source/SwiftLintFramework/Models/Region.swift +++ b/Source/SwiftLintFramework/Models/Region.swift @@ -1,22 +1,42 @@ +/// A contiguous region of Swift source code. public struct Region: Equatable { + /// The location describing the start of the region. All locations that are less than this value + /// (earlier in the source file) are not contained in this region. public let start: Location + /// The location describing the end of the region. All locations that are greater than this value + /// (later in the source file) are not contained in this region. public let end: Location + /// All SwiftLint rule identifiers that are disabled in this region. public let disabledRuleIdentifiers: Set + /// Creates a Region by setting explicit values for all its properties. + /// + /// - parameter start: The region's starting location. + /// - parameter end: The region's ending location. + /// - parameter disabledRuleIdentifiers: All SwiftLint rule identifiers that are disabled in this region. public init(start: Location, end: Location, disabledRuleIdentifiers: Set) { self.start = start self.end = end self.disabledRuleIdentifiers = disabledRuleIdentifiers } + /// Whether the specific location is contained in this region. + /// + /// - parameter location: The location to check for containment. public func contains(_ location: Location) -> Bool { return start <= location && end >= location } + /// Whether the specified rule is enabled in this region. + /// + /// - parameter rule: The rule whose status should be determined. public func isRuleEnabled(_ rule: Rule) -> Bool { return !isRuleDisabled(rule) } + /// Whether the specified rule is disabled in this region. + /// + /// - parameter rule: The rule whose status should be determined. public func isRuleDisabled(_ rule: Rule) -> Bool { guard !disabledRuleIdentifiers.contains(.all) else { return true @@ -27,6 +47,10 @@ public struct Region: Equatable { return !regionIdentifiers.isDisjoint(with: identifiersToCheck) } + /// Returns the deprecated rule aliases that are disabling the specified rule in this region. + /// Returns the empty set if the rule isn't disabled in this region. + /// + /// - parameter rule: The rule to check. public func deprecatedAliasesDisabling(rule: Rule) -> Set { let identifiers = type(of: rule).description.deprecatedAliases return Set(disabledRuleIdentifiers.map { $0.stringRepresentation }).intersection(identifiers) diff --git a/Source/SwiftLintFramework/Models/RuleDescription.swift b/Source/SwiftLintFramework/Models/RuleDescription.swift index 81bcba53c..ffcbe146e 100644 --- a/Source/SwiftLintFramework/Models/RuleDescription.swift +++ b/Source/SwiftLintFramework/Models/RuleDescription.swift @@ -1,21 +1,65 @@ +/// A detailed description for a SwiftLint rule. Used for both documentation and testing purposes. public struct RuleDescription: Equatable, Codable { + /// The rule's unique identifier, to be used in configuration files and SwiftLint commands. + /// Should be short and only comprised of lowercase latin alphabet letters and underscores formatted in snake case. public let identifier: String + + /// The rule's human-readable name. Should be short, descriptive and formatted in Title Case. May contain spaces. public let name: String + + /// The rule's verbose description. Should read as a sentence or short paragraph. Good things to include are an + /// explanation of the rule's purpose and rationale. public let description: String + + /// The `RuleKind` that best categorizes this rule. public let kind: RuleKind + + /// Swift source examples that do not trigger a violation for this rule. Used for documentation purposes to inform + /// users of various samples of code that are considered valid by this rule. Should be valid Swift syntax but is not + /// required to compile. + /// + /// These examples are also used for testing purposes if the rule conforms to `AutomaticTestableRule`. Tests will + /// validate that the rule does not trigger any violations for these examples. public let nonTriggeringExamples: [String] + + /// Swift source examples that do trigger one or more violations for this rule. Used for documentation purposes to + /// inform users of various samples of code that are considered invalid by this rule. Should be valid Swift syntax + /// but is not required to compile. + /// + /// Violations should occur where `↓` markers are located. + /// + /// These examples are also used for testing purposes if the rule conforms to `AutomaticTestableRule`. Tests will + /// validate that the rule triggers violations for these examples wherever `↓` markers are located. public let triggeringExamples: [String] + + /// Pairs of Swift source examples, where keys are examples that trigger violations for this rule, and the values + /// are the expected value after applying corrections with the rule. + /// + /// Rules that aren't correctable (conforming to the `CorrectableRule` protocol) should leave property empty. + /// + /// These examples are used for testing purposes if the rule conforms to `AutomaticTestableRule`. Tests will + /// validate that the rule corrects all keys to their corresponding values. public let corrections: [String: String] + + /// Any previous iteration of the rule's identifier that was previously shipped with SwiftLint. public let deprecatedAliases: Set + + /// The oldest version of the Swift compiler supported by this rule. public let minSwiftVersion: SwiftVersion + + /// Whether or not this rule can only be executed on a file physically on-disk. Typically necessary for rules + /// conforming to `AnalyzerRule`. public let requiresFileOnDisk: Bool + /// The console-printable string for this description. public var consoleDescription: String { return "\(name) (\(identifier)): \(description)" } + /// All identifiers that have been used to uniquely identify this rule in past and current SwiftLint versions. public var allIdentifiers: [String] { return Array(deprecatedAliases) + [identifier] } + /// Creates a `RuleDescription` by specifying all its properties directly. public init(identifier: String, name: String, description: String, kind: RuleKind, minSwiftVersion: SwiftVersion = .three, nonTriggeringExamples: [String] = [], triggeringExamples: [String] = [], diff --git a/Source/SwiftLintFramework/Models/RuleIdentifier.swift b/Source/SwiftLintFramework/Models/RuleIdentifier.swift index 7bc7da0d1..8c433f0a7 100644 --- a/Source/SwiftLintFramework/Models/RuleIdentifier.swift +++ b/Source/SwiftLintFramework/Models/RuleIdentifier.swift @@ -1,9 +1,20 @@ +/// An identifier representing a SwiftLint rule, or all rules. public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral { + // MARK: - Values + + /// Special identifier that should be treated as referring to 'all' SwiftLint rules. One helpful usecase is in + /// disabling all SwiftLint rules in a given file by adding a `// swiftlint:disable all` comment at the top of the + /// file. case all + + /// Represents a single SwiftLint rule with the specified identifier. case single(identifier: String) + // MARK: - Properties + private static let allStringRepresentation = "all" + /// The spelling of the string for this idenfitier. public var stringRepresentation: String { switch self { case .all: @@ -14,10 +25,17 @@ public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral { } } + // MARK: - Initializers + + /// Creates a `RuleIdentifier` by its string representation. + /// + /// - parameter value: The string representation. public init(_ value: String) { self = value == RuleIdentifier.allStringRepresentation ? .all : .single(identifier: value) } + // MARK: - ExpressibleByStringLiteral Conformance + public init(stringLiteral value: String) { self = RuleIdentifier(value) } diff --git a/Source/SwiftLintFramework/Models/RuleKind.swift b/Source/SwiftLintFramework/Models/RuleKind.swift index df628b463..28228d607 100644 --- a/Source/SwiftLintFramework/Models/RuleKind.swift +++ b/Source/SwiftLintFramework/Models/RuleKind.swift @@ -1,7 +1,13 @@ +/// All the possible rule kinds (categories). public enum RuleKind: String, Codable { + /// Describes rules that validate Swift source conventions. case lint + /// Describes rules that validate common practices in the Swift community. case idiomatic + /// Describes rules that validate stylistic choices. case style + /// Describes rules that validate magnitudes or measurements of Swift source. case metrics + /// Describes rules that validate that code patterns with poor performance are avoided. case performance } diff --git a/Source/SwiftLintFramework/Models/RuleList.swift b/Source/SwiftLintFramework/Models/RuleList.swift index 20d5f57c5..be556d8f3 100644 --- a/Source/SwiftLintFramework/Models/RuleList.swift +++ b/Source/SwiftLintFramework/Models/RuleList.swift @@ -1,15 +1,27 @@ +/// All possible configuration errors. public enum RuleListError: Error { + /// The rule list contains more than one configuration for the specified rule. case duplicatedConfigurations(rule: Rule.Type) } +/// A list of available SwiftLint rules. public struct RuleList { + /// The rules contained in this list. public let list: [String: Rule.Type] private let aliases: [String: String] + // MARK: - Initializers + + /// Creates a `RuleList` by specifying all its rules. + /// + /// - parameter rules: The rules to be contained in this list. public init(rules: Rule.Type...) { self.init(rules: rules) } + /// Creates a `RuleList` by specifying all its rules. + /// + /// - parameter rules: The rules to be contained in this list. public init(rules: [Rule.Type]) { var tmpList = [String: Rule.Type]() var tmpAliases = [String: String]() @@ -26,6 +38,8 @@ public struct RuleList { aliases = tmpAliases } + // MARK: - Internal + internal func configuredRules(with dictionary: [String: Any]) throws -> [Rule] { var rules = [String: Rule]() diff --git a/Source/SwiftLintFramework/Models/RuleParameter.swift b/Source/SwiftLintFramework/Models/RuleParameter.swift index 0463884d8..356395baa 100644 --- a/Source/SwiftLintFramework/Models/RuleParameter.swift +++ b/Source/SwiftLintFramework/Models/RuleParameter.swift @@ -1,7 +1,14 @@ +/// A configuration parameter for rules. public struct RuleParameter: Equatable { + /// The severity that should be assigned to the violation of this parameter's value is met. public let severity: ViolationSeverity + /// The value to configure the rule. public let value: T + /// Creates a `RuleParameter` by specifying its properties directly. + /// + /// - parameter severity: The severity that should be assigned to the violation of this parameter's value is met. + /// - parameter value: The value to configure the rule. public init(severity: ViolationSeverity, value: T) { self.severity = severity self.value = value diff --git a/Source/SwiftLintFramework/Models/RuleStorage.swift b/Source/SwiftLintFramework/Models/RuleStorage.swift index 9bdb1a023..be791ecfa 100644 --- a/Source/SwiftLintFramework/Models/RuleStorage.swift +++ b/Source/SwiftLintFramework/Models/RuleStorage.swift @@ -1,13 +1,20 @@ import Dispatch +/// A storage mechanism for aggregating the results of `CollectingRule`s. public class RuleStorage { private var storage: [ObjectIdentifier: [SwiftLintFile: Any]] private let access = DispatchQueue(label: "io.realm.swiftlint.ruleStorageAccess", attributes: .concurrent) + /// Creates a `RuleStorage` with no initial stored data. public init() { storage = [:] } + /// Collects file info for a given rule into the storage.s + /// + /// - parameter info: The file information to store. + /// - parameter file: The file for which this information pertains to. + /// - parameter rule: The SwiftLint rule that generated this info. func collect(info: R.FileInfo, for file: SwiftLintFile, in rule: R) { let key = ObjectIdentifier(R.self) access.sync(flags: .barrier) { @@ -15,6 +22,9 @@ public class RuleStorage { } } + /// Retrieves all file information for a given rule that was collected via `collect(...)`. + /// + /// - parameter rule: The rule whose collected information should be retrieved. func collectedInfo(for rule: R) -> [SwiftLintFile: R.FileInfo]? { return access.sync { storage[ObjectIdentifier(R.self)] as? [SwiftLintFile: R.FileInfo] diff --git a/Source/SwiftLintFramework/Models/StyleViolation.swift b/Source/SwiftLintFramework/Models/StyleViolation.swift index 2eb9b76d4..ace172fae 100644 --- a/Source/SwiftLintFramework/Models/StyleViolation.swift +++ b/Source/SwiftLintFramework/Models/StyleViolation.swift @@ -1,12 +1,28 @@ +/// A value describing an instance of Swift source code that is considered invalid by a SwiftLint rule. public struct StyleViolation: CustomStringConvertible, Equatable, Codable { + /// The description of the rule that generated this violation. public let ruleDescription: RuleDescription + + /// The severity of this violation. public let severity: ViolationSeverity + + /// The location of this violation. public let location: Location + + /// The justification for this violation. public let reason: String + + /// A printable description for this violation. public var description: String { return XcodeReporter.generateForSingleViolation(self) } + /// Creates a `StyleViolation` by specifying its properties directly. + /// + /// - parameter ruleDescription: The description of the rule that generated this violation. + /// - parameter severity: The severity of this violation. + /// - parameter location: The location of this violation. + /// - parameter reason: The justification for this violation. public init(ruleDescription: RuleDescription, severity: ViolationSeverity = .warning, location: Location, reason: String? = nil) { self.ruleDescription = ruleDescription diff --git a/Source/SwiftLintFramework/Models/SwiftLintFile.swift b/Source/SwiftLintFramework/Models/SwiftLintFile.swift index 7eb032fc4..95ead71bb 100644 --- a/Source/SwiftLintFramework/Models/SwiftLintFile.swift +++ b/Source/SwiftLintFramework/Models/SwiftLintFile.swift @@ -1,11 +1,12 @@ import Foundation import SourceKittenFramework +/// A unit of Swift source code, either on disk or in memory. public final class SwiftLintFile { private static var id = 0 private static var lock = NSLock() - private static func nextId () -> Int { + private static func nextID () -> Int { lock.lock() defer { lock.unlock() } id += 1 @@ -15,41 +16,61 @@ public final class SwiftLintFile { let file: File let id: Int + /// Creates a `SwiftLintFile` with a SourceKitten `File`. + /// + /// - parameter file: A file from SourceKitten. public init(file: File) { self.file = file - self.id = SwiftLintFile.nextId() + self.id = SwiftLintFile.nextID() } + /// Creates a `SwiftLintFile` by specifying its path on disk. + /// Fails if the file does not exist. + /// + /// - parameter path: The path to a file on disk. Relative and absolute paths supported. public convenience init?(path: String) { guard let file = File(path: path) else { return nil } self.init(file: file) } + /// Creates a `SwiftLintFile` by specifying its path on disk. Unlike the `SwiftLintFile(path:)` initializer, this + /// one does not read its contents immediately, but rather traps at runtime when attempting to access its contents. + /// + /// - parameter path: The path to a file on disk. Relative and absolute paths supported. public convenience init(pathDeferringReading path: String) { self.init(file: File(pathDeferringReading: path)) } + /// Creates a `SwiftLintFile` that is not backed by a file on disk by specifying its contents. + /// + /// - parameter contents: The contents of the file. public convenience init(contents: String) { self.init(file: File(contents: contents)) } + /// The path on disk for this file. public var path: String? { return file.path } + /// The file's contents. public var contents: String { return file.contents } + /// A string view into the contents of this file optimized for string manipulation operations. public var stringView: StringView { return file.stringView } + /// The parsed lines for this file's contents. public var lines: [Line] { return file.lines } } +// MARK: - Hashable Conformance + extension SwiftLintFile: Hashable { public static func == (lhs: SwiftLintFile, rhs: SwiftLintFile) -> Bool { return lhs.id == rhs.id diff --git a/Source/SwiftLintFramework/Models/SwiftLintSyntaxMap.swift b/Source/SwiftLintFramework/Models/SwiftLintSyntaxMap.swift index e26653b51..8365a806b 100644 --- a/Source/SwiftLintFramework/Models/SwiftLintSyntaxMap.swift +++ b/Source/SwiftLintFramework/Models/SwiftLintSyntaxMap.swift @@ -1,18 +1,25 @@ import Foundation import SourceKittenFramework +/// Represents a Swift file's syntax information. public struct SwiftLintSyntaxMap { + /// The raw `SyntaxMap` obtained by SourceKitten. public let value: SyntaxMap + + /// The SwiftLint-specific syntax tokens for this syntax map. public let tokens: [SwiftLintSyntaxToken] + /// Creates a `SwiftLintSyntaxMap` from the raw `SyntaxMap` obtained by SourceKitten. + /// + /// - arameter value: The raw `SyntaxMap` obtained by SourceKitten. public init(value: SyntaxMap) { self.value = value self.tokens = value.tokens.map(SwiftLintSyntaxToken.init) } - /// Returns array of SyntaxTokens intersecting with byte range + /// Returns array of SyntaxTokens intersecting with byte range. /// - /// - Parameter byteRange: byte based NSRange + /// - parameter byteRange: Byte-based NSRange. internal func tokens(inByteRange byteRange: NSRange) -> [SwiftLintSyntaxToken] { func intersect(_ token: SwiftLintSyntaxToken) -> Bool { return token.range.intersects(byteRange) @@ -35,6 +42,9 @@ public struct SwiftLintSyntaxMap { return Array(tokensAfterFirstIntersection) } + /// Returns the syntax kinds in the specified byte range. + /// + /// - parameter byteRange: Byte-based NSRange. internal func kinds(inByteRange byteRange: NSRange) -> [SyntaxKind] { return tokens(inByteRange: byteRange).compactMap { $0.kind } } diff --git a/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift b/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift index a25eb84e9..d8a333972 100644 --- a/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift +++ b/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift @@ -1,23 +1,33 @@ import Foundation import SourceKittenFramework +/// A SwiftLint-aware Swift syntax token. public struct SwiftLintSyntaxToken { + /// The raw `SyntaxToken` obtained by SourceKitten. public let value: SyntaxToken + + /// The syntax kind associated with is token. public let kind: SyntaxKind? + /// Creates a `SwiftLintSyntaxToken` from the raw `SyntaxToken` obtained by SourceKitten. + /// + /// - parameter value: The raw `SyntaxToken` obtained by SourceKitten. public init(value: SyntaxToken) { self.value = value kind = SyntaxKind(rawValue: value.type) } + /// The byte range in a source file for this token. public var range: NSRange { return NSRange(location: value.offset, length: value.length) } + /// The starting byte offset in a source file for this token. public var offset: Int { return value.offset } + /// The length in bytes for this token. public var length: Int { return value.length } diff --git a/Source/SwiftLintFramework/Models/SwiftVersion.swift b/Source/SwiftLintFramework/Models/SwiftVersion.swift index 8f2865da5..0cb6193f6 100644 --- a/Source/SwiftLintFramework/Models/SwiftVersion.swift +++ b/Source/SwiftLintFramework/Models/SwiftVersion.swift @@ -1,7 +1,8 @@ import Foundation import SourceKittenFramework -public struct SwiftVersion: RawRepresentable, Codable { +/// A value describing the version of the Swift compiler. +public struct SwiftVersion: RawRepresentable, Codable, Comparable { public typealias RawValue = String public let rawValue: String @@ -9,23 +10,29 @@ public struct SwiftVersion: RawRepresentable, Codable { public init(rawValue: String) { self.rawValue = rawValue } -} -extension SwiftVersion: Comparable { - // Comparable public static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool { return lhs.rawValue < rhs.rawValue } } public extension SwiftVersion { + /// Swift 3.x - https://swift.org/download/#swift-30 static let three = SwiftVersion(rawValue: "3.0.0") + /// Swift 4.0.x - https://swift.org/download/#swift-40 static let four = SwiftVersion(rawValue: "4.0.0") + /// Swift 4.1.x - https://swift.org/download/#swift-41 static let fourDotOne = SwiftVersion(rawValue: "4.1.0") + /// Swift 4.2.x - https://swift.org/download/#swift-42 static let fourDotTwo = SwiftVersion(rawValue: "4.2.0") + /// Swift 5.0.x - https://swift.org/download/#swift-50 static let five = SwiftVersion(rawValue: "5.0.0") + /// Swift 5.1.x - https://swift.org/download/#swift-51 static let fiveDotOne = SwiftVersion(rawValue: "5.1.0") + /// The current detected Swift compiler version, based on the currently accessible SourceKit version. + /// + /// - note: Override by setting the `SWIFTLINT_SWIFT_VERSION` environment variable. static let current: SwiftVersion = { // Allow forcing the Swift version, useful in cases where SourceKit isn't available if let envVersion = ProcessInfo.processInfo.environment["SWIFTLINT_SWIFT_VERSION"] { diff --git a/Source/SwiftLintFramework/Models/Version.swift b/Source/SwiftLintFramework/Models/Version.swift index 511794370..552fdaaee 100644 --- a/Source/SwiftLintFramework/Models/Version.swift +++ b/Source/SwiftLintFramework/Models/Version.swift @@ -1,5 +1,8 @@ +/// A type describing the SwiftLint version. public struct Version { + /// The string value for this version. public let value: String + /// The current SwiftLint version. public static let current = Version(value: "0.38.1") } diff --git a/Source/SwiftLintFramework/Models/ViolationSeverity.swift b/Source/SwiftLintFramework/Models/ViolationSeverity.swift index f1d0f8443..adb64c92e 100644 --- a/Source/SwiftLintFramework/Models/ViolationSeverity.swift +++ b/Source/SwiftLintFramework/Models/ViolationSeverity.swift @@ -1,5 +1,8 @@ +/// The magnitude of a `StyleViolation`. public enum ViolationSeverity: String, Comparable, Codable { + /// Non-fatal. If using SwiftLint as an Xcode build phase, Xcode will mark the build as having succeeded. case warning + /// Fatal. If using SwiftLint as an Xcode build phase, Xcode will mark the build as having failed. case error // MARK: Comparable diff --git a/Source/SwiftLintFramework/Models/YamlParser.swift b/Source/SwiftLintFramework/Models/YamlParser.swift index 8e9b57818..23846ec51 100644 --- a/Source/SwiftLintFramework/Models/YamlParser.swift +++ b/Source/SwiftLintFramework/Models/YamlParser.swift @@ -9,7 +9,12 @@ internal enum YamlParserError: Error, Equatable { // MARK: - YamlParser +/// An interface for parsing YAML. public struct YamlParser { + /// Parses the input YAML string as an untyped dictionary. + /// + /// - parameter yaml: YAML-formatted string. + /// - parameter env: The environment to use to expand variables in the YAML. public static func parse(_ yaml: String, env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] { do { diff --git a/Source/SwiftLintFramework/Protocols/ASTRule.swift b/Source/SwiftLintFramework/Protocols/ASTRule.swift index c82b9ebdc..d809a9720 100644 --- a/Source/SwiftLintFramework/Protocols/ASTRule.swift +++ b/Source/SwiftLintFramework/Protocols/ASTRule.swift @@ -1,8 +1,20 @@ import SourceKittenFramework +/// A rule that leverages the Swift source's pre-typechecked Abstract Syntax Tree to recurse into the source's +/// structure, validating the rule recursively in nested source bodies. public protocol ASTRule: Rule { + /// The kind of token being recursed over. associatedtype KindType: RawRepresentable + + /// Executes the rule on a file and a subset of its AST structure, returnng any violations to the rule's + /// expectations. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter kind: The kind of token being recursed over. + /// - parameter dictionary: The dicttionary for an AST subset to validate. func validate(file: SwiftLintFile, kind: KindType, dictionary: SourceKittenDictionary) -> [StyleViolation] + + /// Get the `kind` from the specified dictionary. func kind(from dictionary: SourceKittenDictionary) -> KindType? } @@ -11,6 +23,11 @@ public extension ASTRule { return validate(file: file, dictionary: file.structureDictionary) } + /// Executes the rule on a file and a subset of its AST structure, returnng any violations to the rule's + /// expectations. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter dictionary: The dicttionary for an AST subset to validate. func validate(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [StyleViolation] { return dictionary.traverseDepthFirst { subDict in guard let kind = self.kind(from: subDict) else { return nil } diff --git a/Source/SwiftLintFramework/Protocols/Reporter.swift b/Source/SwiftLintFramework/Protocols/Reporter.swift index 7449a0c2f..81db59e9b 100644 --- a/Source/SwiftLintFramework/Protocols/Reporter.swift +++ b/Source/SwiftLintFramework/Protocols/Reporter.swift @@ -1,10 +1,24 @@ +/// An interface for reporting violations as strings. public protocol Reporter: CustomStringConvertible { + /// The unique identifier for this reporter. static var identifier: String { get } + + /// Whether or not this reporter can output incrementally as violations are found or if all violations must be + /// collected before generating the report. static var isRealtime: Bool { get } + /// Return a string with the report for the specified violations. + /// + /// - parameter violations: The violations to report. + /// + /// - returns: The report. static func generateReport(_ violations: [StyleViolation]) -> String } +/// Returns the reporter with the specified identifier. Traps if the specified identifier doesn't correspond to any +/// known reporters. +/// +/// - parameter identifier: The identifier corresponding to the reporter. public func reporterFrom(identifier: String) -> Reporter.Type { // swiftlint:disable:this cyclomatic_complexity switch identifier { case XcodeReporter.identifier: diff --git a/Source/SwiftLintFramework/Protocols/Rule.swift b/Source/SwiftLintFramework/Protocols/Rule.swift index e5c53d550..f7af0693b 100644 --- a/Source/SwiftLintFramework/Protocols/Rule.swift +++ b/Source/SwiftLintFramework/Protocols/Rule.swift @@ -1,19 +1,53 @@ import Foundation import SourceKittenFramework +/// An executable value that can identify issues (violations) in Swift source code. public protocol Rule { + /// A verbose description of many of this rule's properties. static var description: RuleDescription { get } + + /// A description of how this rule has been configured to run. var configurationDescription: String { get } - init() // Rules need to be able to be initialized with default values + /// A default initializer for rules. All rules need to be trivially initializable. + init() + + /// Creates a rule by applying its configuration. + /// + /// - throws: Throws if the configuration didn't match the expected format. init(configuration: Any) throws + /// Executes the rule on a file and returns any violations to the rule's expectations. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] + + /// Executes the rule on a file and returns any violations to the rule's expectations. + /// + /// - parameter file: The file for which to execute the rule. func validate(file: SwiftLintFile) -> [StyleViolation] + + /// Whether or not the specified rule is equivalent to the current rule. func isEqualTo(_ rule: Rule) -> Bool - // These are called by the linter and are always implemented in extensions. + /// Collects information for the specified file in a storage object, to be analyzed by a `CollectedLinter`. + /// + /// - note: This function is called by the linter and is always implemented in extensions. + /// + /// - parameter file: The file for which to collect info. + /// - parameter storage: The storage object where collected info should be saved. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func collectInfo(for file: SwiftLintFile, into storage: RuleStorage, compilerArguments: [String]) + + /// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's + /// expectations. + /// + /// - note: This function is called by the linter and is always implemented in extensions. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter storage: The storage object containing all collected info. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func validate(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation] } @@ -40,21 +74,46 @@ extension Rule { } } +/// A rule that is not enabled by default. Rules conforming to this need to be explicitly enabled by users. public protocol OptInRule: Rule {} +/// A rule that has unit tests automatically generated using Sourcery. public protocol AutomaticTestableRule: Rule {} +/// A rule that is user-configurable. public protocol ConfigurationProviderRule: Rule { + /// The type of configuration used to configure this rule. associatedtype ConfigurationType: RuleConfiguration + /// This rule's configuration. var configuration: ConfigurationType { get set } } +/// A rule that can correct violations. public protocol CorrectableRule: Rule { + /// Attempts to correct the violations to this rule in the specified file. + /// + /// - parameter file: The file for which to correct violations. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. + /// + /// - returns: All corrections that were applied. func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] + + /// Attempts to correct the violations to this rule in the specified file. + /// + /// - parameter file: The file for which to correct violations. + /// + /// - returns: All corrections that were applied. func correct(file: SwiftLintFile) -> [Correction] - // Called by the linter and are always implemented in extensions. + /// Attempts to correct the violations to this rule in the specified file after collecting file info for all files + /// and returns all corrections that were applied. + /// + /// - note: This function is called by the linter and is always implemented in extensions. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter storage: The storage object containing all collected info. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func correct(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [Correction] } @@ -67,8 +126,13 @@ public extension CorrectableRule { } } +/// A correctable rule that can apply its corrections by replacing the content of ranges in the offending file with +/// updated content. public protocol SubstitutionCorrectableRule: CorrectableRule { + /// Returns the NSString-based `NSRange`s to be replaced in the specified file. func violationRanges(in file: SwiftLintFile) -> [NSRange] + + /// Returns the substitution to apply for the given range. func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? } @@ -94,7 +158,9 @@ public extension SubstitutionCorrectableRule { } } +/// A `SubstitutionCorrectableRule` that is also an `ASTRule`. public protocol SubstitutionCorrectableASTRule: SubstitutionCorrectableRule, ASTRule { + /// Returns the NSString-based `NSRange`s to be replaced in the specified file. func violationRanges(in file: SwiftLintFile, kind: KindType, dictionary: SourceKittenDictionary) -> [NSRange] } @@ -108,8 +174,11 @@ public extension SubstitutionCorrectableASTRule { } } +/// A rule that does not need SourceKit to operate and can still operate even after SourceKit has crashed. public protocol SourceKitFreeRule: Rule {} +/// A rule that can operate on the post-typechecked AST using compiler arguments. Performs rules that are more like +/// static analysis than syntactic checks. public protocol AnalyzerRule: OptInRule {} public extension AnalyzerRule { @@ -118,6 +187,7 @@ public extension AnalyzerRule { } } +/// :nodoc: public extension AnalyzerRule where Self: CorrectableRule { func correct(file: SwiftLintFile) -> [Correction] { queuedFatalError("Must call `correct(file:compilerArguments:)` for AnalyzerRule") @@ -129,12 +199,36 @@ public extension AnalyzerRule where Self: CorrectableRule { /// Type-erased protocol used to check whether a rule is collectable. public protocol AnyCollectingRule: Rule { } +/// A rule that requires knowledge of all other files being linted. public protocol CollectingRule: AnyCollectingRule { + /// The kind of information to collect for each file being linted for this rule. associatedtype FileInfo + + /// Collects information for the specified file, to be analyzed by a `CollectedLinter`. + /// + /// - parameter file: The file for which to collect info. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> FileInfo + + /// Collects information for the specified file, to be analyzed by a `CollectedLinter`. + /// + /// - parameter file: The file for which to collect info. func collectInfo(for file: SwiftLintFile) -> FileInfo + + /// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's + /// expectations. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter collectedInfo: All collected info for all files. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo], compilerArguments: [String]) -> [StyleViolation] + + /// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's + /// expectations. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter collectedInfo: All collected info for all files. func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [StyleViolation] } @@ -182,9 +276,26 @@ public extension CollectingRule where Self: AnalyzerRule { } } +/// A `CollectingRule` that is also a `CorrectableRule`. public protocol CollectingCorrectableRule: CollectingRule, CorrectableRule { + /// Attempts to correct the violations to this rule in the specified file after collecting file info for all files + /// and returns all corrections that were applied. + /// + /// - note: This function is called by the linter and is always implemented in extensions. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter collectedInfo: All collected info. + /// - parameter compilerArguments: The compiler arguments needed to compile this file. func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo], compilerArguments: [String]) -> [Correction] + + /// Attempts to correct the violations to this rule in the specified file after collecting file info for all files + /// and returns all corrections that were applied. + /// + /// - note: This function is called by the linter and is always implemented in extensions. + /// + /// - parameter file: The file for which to execute the rule. + /// - parameter collectedInfo: All collected info. func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [Correction] } @@ -239,6 +350,7 @@ public extension ConfigurationProviderRule { // MARK: - == Implementations +/// :nodoc: public extension Array where Element == Rule { static func == (lhs: Array, rhs: Array) -> Bool { if lhs.count != rhs.count { return false } diff --git a/Source/SwiftLintFramework/Protocols/RuleConfiguration.swift b/Source/SwiftLintFramework/Protocols/RuleConfiguration.swift index e2f924a67..2c36af862 100644 --- a/Source/SwiftLintFramework/Protocols/RuleConfiguration.swift +++ b/Source/SwiftLintFramework/Protocols/RuleConfiguration.swift @@ -1,7 +1,14 @@ +/// A configuration value for a rule to allow users to modify its behavior. public protocol RuleConfiguration { + /// A human-readable description for this configuration and its applied values. var consoleDescription: String { get } + /// Apply an untyped configuration to the current value. + /// + /// - throws: Throws if the configuration is not in the expected format. mutating func apply(configuration: Any) throws + + /// Whether the specified configuration is equivalent to the current value. func isEqualTo(_ ruleConfiguration: RuleConfiguration) -> Bool } diff --git a/Source/SwiftLintFramework/Reporters/CSVReporter.swift b/Source/SwiftLintFramework/Reporters/CSVReporter.swift index 42f7630dc..4413e6b18 100644 --- a/Source/SwiftLintFramework/Reporters/CSVReporter.swift +++ b/Source/SwiftLintFramework/Reporters/CSVReporter.swift @@ -1,16 +1,9 @@ import Foundation -private extension String { - func escapedForCSV() -> String { - let escapedString = replacingOccurrences(of: "\"", with: "\"\"") - if escapedString.contains(",") || escapedString.contains("\n") { - return "\"\(escapedString)\"" - } - return escapedString - } -} - +/// Reports violations as a newline-separated string of comma-separated values (CSV). public struct CSVReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "csv" public static let isRealtime = false @@ -33,7 +26,9 @@ public struct CSVReporter: Reporter { return rows.joined(separator: "\n") } - fileprivate static func csvRow(for violation: StyleViolation) -> String { + // MARK: - Private + + private static func csvRow(for violation: StyleViolation) -> String { return [ violation.location.file?.escapedForCSV() ?? "", violation.location.line?.description ?? "", @@ -45,3 +40,13 @@ public struct CSVReporter: Reporter { ].joined(separator: ",") } } + +private extension String { + func escapedForCSV() -> String { + let escapedString = replacingOccurrences(of: "\"", with: "\"\"") + if escapedString.contains(",") || escapedString.contains("\n") { + return "\"\(escapedString)\"" + } + return escapedString + } +} diff --git a/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift b/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift index d9ee93f67..f7fcce399 100644 --- a/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift +++ b/Source/SwiftLintFramework/Reporters/CheckstyleReporter.swift @@ -1,4 +1,8 @@ +/// Reports violations as XML conforming to the Checkstyle specification, as defined here: +/// https://www.jetbrains.com/help/teamcity/xml-report-processing.html public struct CheckstyleReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "checkstyle" public static let isRealtime = false @@ -17,6 +21,8 @@ public struct CheckstyleReporter: Reporter { ].joined() } + // MARK: - Private + private static func generateForViolationFile(_ file: String, violations: [StyleViolation]) -> String { return [ "\n\t\n", diff --git a/Source/SwiftLintFramework/Reporters/EmojiReporter.swift b/Source/SwiftLintFramework/Reporters/EmojiReporter.swift index 9201f4a27..b4d1de225 100644 --- a/Source/SwiftLintFramework/Reporters/EmojiReporter.swift +++ b/Source/SwiftLintFramework/Reporters/EmojiReporter.swift @@ -1,4 +1,7 @@ +/// Reports violations in the format that's both fun and easy to read. public struct EmojiReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "emoji" public static let isRealtime = false @@ -13,6 +16,8 @@ public struct EmojiReporter: Reporter { .map(report).joined(separator: "\n") } + // MARK: - Private + private static func report(for file: String, with violations: [StyleViolation]) -> String { let lines = [file] + violations.sorted { lhs, rhs in guard lhs.severity == rhs.severity else { diff --git a/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift b/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift index 752880de3..02ce5a95e 100644 --- a/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift +++ b/Source/SwiftLintFramework/Reporters/GitHubActionsLoggingReporter.swift @@ -1,4 +1,7 @@ +/// Reports violations in the format GitHub-hosted virtual machine for Actions can recognize as messages. public struct GitHubActionsLoggingReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "github-actions-logging" public static let isRealtime = true @@ -10,7 +13,9 @@ public struct GitHubActionsLoggingReporter: Reporter { return violations.map(generateForSingleViolation).joined(separator: "\n") } - internal static func generateForSingleViolation(_ violation: StyleViolation) -> String { + // MARK: - Private + + private static func generateForSingleViolation(_ violation: StyleViolation) -> String { // swiftlint:disable:next line_length // https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands // ::(warning|error) file={relative_path_to_file},line={:line},col={:character}::{content} diff --git a/Source/SwiftLintFramework/Reporters/HTMLReporter.swift b/Source/SwiftLintFramework/Reporters/HTMLReporter.swift index d485c1692..f0decb4f3 100644 --- a/Source/SwiftLintFramework/Reporters/HTMLReporter.swift +++ b/Source/SwiftLintFramework/Reporters/HTMLReporter.swift @@ -6,12 +6,15 @@ private let formatter: DateFormatter = { return formatter }() +/// Reports violations as HTML. public struct HTMLReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "html" public static let isRealtime = false public var description: String { - return "Reports violations as HTML" + return "Reports violations as HTML." } public static func generateReport(_ violations: [StyleViolation]) -> String { @@ -19,6 +22,8 @@ public struct HTMLReporter: Reporter { dateString: formatter.string(from: Date())) } + // MARK: - Internal + // swiftlint:disable:next function_body_length internal static func generateReport(_ violations: [StyleViolation], swiftlintVersion: String, dateString: String) -> String { @@ -143,6 +148,8 @@ public struct HTMLReporter: Reporter { ].joined() } + // MARK: - Private + private static func generateSingleRow(for violation: StyleViolation, at index: Int) -> String { let severity: String = violation.severity.rawValue.capitalized let location = violation.location diff --git a/Source/SwiftLintFramework/Reporters/JSONReporter.swift b/Source/SwiftLintFramework/Reporters/JSONReporter.swift index b4371bb5c..bed7b1514 100644 --- a/Source/SwiftLintFramework/Reporters/JSONReporter.swift +++ b/Source/SwiftLintFramework/Reporters/JSONReporter.swift @@ -1,7 +1,10 @@ import Foundation import SourceKittenFramework +/// Reports violations as a JSON array. public struct JSONReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "json" public static let isRealtime = false @@ -13,7 +16,9 @@ public struct JSONReporter: Reporter { return toJSON(violations.map(dictionary(for:))) } - fileprivate static func dictionary(for violation: StyleViolation) -> [String: Any] { + // MARK: - Private + + private static func dictionary(for violation: StyleViolation) -> [String: Any] { return [ "file": violation.location.file ?? NSNull() as Any, "line": violation.location.line ?? NSNull() as Any, diff --git a/Source/SwiftLintFramework/Reporters/JUnitReporter.swift b/Source/SwiftLintFramework/Reporters/JUnitReporter.swift index f28fa8553..d246588e3 100644 --- a/Source/SwiftLintFramework/Reporters/JUnitReporter.swift +++ b/Source/SwiftLintFramework/Reporters/JUnitReporter.swift @@ -1,4 +1,7 @@ +/// Reports violations as JUnit XML. public struct JUnitReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "junit" public static let isRealtime = false diff --git a/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift b/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift index 7f984034d..7b4ae3572 100644 --- a/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift +++ b/Source/SwiftLintFramework/Reporters/MarkdownReporter.swift @@ -1,21 +1,14 @@ import Foundation -private extension String { - func escapedForMarkdown() -> String { - let escapedString = replacingOccurrences(of: "\"", with: "\"\"") - if escapedString.contains("|") || escapedString.contains("\n") { - return "\"\(escapedString)\"" - } - return escapedString - } -} - +/// Reports violations as markdown formated (with tables). public struct MarkdownReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "markdown" public static let isRealtime = false public var description: String { - return "Reports violations as markdown formated (with tables)" + return "Reports violations as markdown formated (with tables)." } public static func generateReport(_ violations: [StyleViolation]) -> String { @@ -31,7 +24,9 @@ public struct MarkdownReporter: Reporter { return rows.joined(separator: "\n") } - fileprivate static func markdownRow(for violation: StyleViolation) -> String { + // MARK: - Private + + private static func markdownRow(for violation: StyleViolation) -> String { return [ violation.location.file?.escapedForMarkdown() ?? "", violation.location.line?.description ?? "", @@ -41,7 +36,7 @@ public struct MarkdownReporter: Reporter { ].joined(separator: " | ") } - fileprivate static func severity(for severity: ViolationSeverity) -> String { + private static func severity(for severity: ViolationSeverity) -> String { switch severity { case .error: return ":stop\\_sign:" @@ -50,3 +45,13 @@ public struct MarkdownReporter: Reporter { } } } + +private extension String { + func escapedForMarkdown() -> String { + let escapedString = replacingOccurrences(of: "\"", with: "\"\"") + if escapedString.contains("|") || escapedString.contains("\n") { + return "\"\(escapedString)\"" + } + return escapedString + } +} diff --git a/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift b/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift index 067ef7437..de1a2ebda 100644 --- a/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift +++ b/Source/SwiftLintFramework/Reporters/SonarQubeReporter.swift @@ -1,6 +1,9 @@ import SourceKittenFramework +/// Reports violations in SonarQube import format. public struct SonarQubeReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "sonarqube" public static let isRealtime = false @@ -12,6 +15,8 @@ public struct SonarQubeReporter: Reporter { return toJSON(["issues": violations.map(dictionary(for:))]) } + // MARK: - Private + // refer to https://docs.sonarqube.org/display/SONAR/Generic+Issue+Data private static func dictionary(for violation: StyleViolation) -> [String: Any] { return [ diff --git a/Source/SwiftLintFramework/Reporters/XcodeReporter.swift b/Source/SwiftLintFramework/Reporters/XcodeReporter.swift index 0ac7843e7..2d7d1e256 100644 --- a/Source/SwiftLintFramework/Reporters/XcodeReporter.swift +++ b/Source/SwiftLintFramework/Reporters/XcodeReporter.swift @@ -1,4 +1,7 @@ +/// Reports violations in the format Xcode uses to display in the IDE. (default) public struct XcodeReporter: Reporter { + // MARK: - Reporter Conformance + public static let identifier = "xcode" public static let isRealtime = true @@ -10,6 +13,9 @@ public struct XcodeReporter: Reporter { return violations.map(generateForSingleViolation).joined(separator: "\n") } + /// Generates a report for a single violation. + /// + /// - parameter violation: The violation to report. internal static func generateForSingleViolation(_ violation: StyleViolation) -> String { // {full_path_to_file}{:line}{:character}: {error,warning}: {content} return [ diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/ModifierOrderConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/ModifierOrderConfiguration.swift index 8f1e7c24e..20c9ca6ad 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/ModifierOrderConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/ModifierOrderConfiguration.swift @@ -8,7 +8,11 @@ public struct ModifierOrderConfiguration: RuleConfiguration, Equatable { return severityConfiguration.consoleDescription + ", preferred_modifier_order: \(preferredModifierOrder)" } - public init(preferredModifierOrder: [SwiftDeclarationAttributeKind.ModifierGroup] = []) { + public init() { + self.preferredModifierOrder = [] + } + + init(preferredModifierOrder: [SwiftDeclarationAttributeKind.ModifierGroup] = []) { self.preferredModifierOrder = preferredModifierOrder } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d522e9758..b3e719eb8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -106,17 +106,23 @@ jobs: - job: jazzy pool: - vmImage: 'Ubuntu 16.04' - container: norionomura/jazzy:0.13.0_swift-5.1.3 + vmImage: 'macOS 10.14' + variables: + DEVELOPER_DIR: /Applications/Xcode_11.3.app steps: - script: swift run swiftlint generate-docs displayName: Run swiftlint generate-docs - script: bundle install --path vendor/bundle displayName: bundle install - - script: sourcekitten doc --spm-module SwiftLintFramework > docs.json - displayName: Generate documentation json - - script: bundle exec jazzy --sourcekitten-sourcefile docs.json + - script: bundle exec jazzy displayName: Run jazzy + - script: > + if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then + echo "Undocumented declarations:" + cat docs/undocumented.json + exit 1 + fi + displayName: Validate documentation coverage - task: PublishPipelineArtifact@0 inputs: artifactName: 'API Docs'