Add documentation comments to all public declarations (#3027)

This commit is contained in:
JP Simard 2020-01-08 09:47:10 -08:00 committed by GitHub
parent df6cf175d8
commit 37167a8a35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 693 additions and 79 deletions

View File

@ -18,3 +18,13 @@ custom_categories:
- name: Rules - name: Rules
children: children:
- Rule Directory - Rule Directory
- name: Reporters
children:
- CSVReporter
- CheckstyleReporter
- EmojiReporter
- GitHubActionsLoggingReporter
- HTMLReporter
- JSONReporter
- JUnitReporter
- MarkdownReporter

View File

@ -1,3 +1,4 @@
/// The rule list containing all available rules built into SwiftLint.
public let masterRuleList = RuleList(rules: [ 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 %} {% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
{% endfor %}]) {% endfor %}])

View File

@ -1,5 +1,5 @@
/// User-facing documentation for a SwiftLint rule. /// User-facing documentation for a SwiftLint rule.
public struct RuleDocumentation { struct RuleDocumentation {
private let ruleType: Rule.Type private let ruleType: Rule.Type
/// Creates a RuleDocumentation instance from a Rule type. /// Creates a RuleDocumentation instance from a Rule type.

View File

@ -37,6 +37,8 @@ extension Configuration {
return cachedConfigurationsByPath[path] 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 { public func withPrecomputedCacheDescription() -> Configuration {
var result = self var result = self
result.computedCacheDescription = result.cacheDescription result.computedCacheDescription = result.cacheDescription

View File

@ -1,10 +1,17 @@
public extension Configuration { public extension Configuration {
/// The style of indentation used in a Swift project.
enum IndentationStyle: Equatable { enum IndentationStyle: Equatable {
/// Swift source code should be indented using tabs.
case tabs case tabs
/// Swift source code should be indented using spaces with `count` spaces per indentation level.
case spaces(count: Int) case spaces(count: Int)
/// The default indentation style if none is explicitly provided.
public static var `default` = spaces(count: 4) 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?) { internal init?(_ object: Any?) {
switch object { switch object {
case let value as Int: self = .spaces(count: value) case let value as Int: self = .spaces(count: value)

View File

@ -1,11 +1,26 @@
import Foundation import Foundation
extension Configuration { 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] { public func lintableFiles(inPath path: String, forceExclude: Bool) -> [SwiftLintFile] {
return lintablePaths(inPath: path, forceExclude: forceExclude) return lintablePaths(inPath: path, forceExclude: forceExclude)
.compactMap(SwiftLintFile.init(pathDeferringReading:)) .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, internal func lintablePaths(inPath path: String, forceExclude: Bool,
fileManager: LintableFileManager = FileManager.default) -> [String] { fileManager: LintableFileManager = FileManager.default) -> [String] {
// If path is a file and we're not forcing excludes, skip filtering with excluded/included paths // 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) 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, public func filterExcludedPaths(fileManager: LintableFileManager = FileManager.default,
in paths: [String]...) -> [String] { in paths: [String]...) -> [String] {
let allPaths = paths.flatMap { $0 } let allPaths = paths.flatMap { $0 }
@ -36,6 +55,12 @@ extension Configuration {
return result.map { $0 as! String } 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] { internal func excludedPaths(fileManager: LintableFileManager) -> [String] {
return excluded return excluded
.flatMap(Glob.resolveGlob) .flatMap(Glob.resolveGlob)

View File

@ -2,6 +2,12 @@ import Foundation
import SourceKittenFramework import SourceKittenFramework
extension Configuration { 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 { public func configuration(for file: SwiftLintFile) -> Configuration {
if let containingDir = file.path?.bridge().deletingLastPathComponent { if let containingDir = file.path?.bridge().deletingLastPathComponent {
return configuration(forPath: containingDir) return configuration(forPath: containingDir)

View File

@ -52,6 +52,14 @@ extension Configuration {
return .default 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, public init?(dict: [String: Any], ruleList: RuleList = masterRuleList, enableAllRules: Bool = false,
cachePath: String? = nil, customRulesIdentifiers: [String] = []) { cachePath: String? = nil, customRulesIdentifiers: [String] = []) {
// Use either new 'opt_in_rules' or deprecated 'enabled_rules' for now. // Use either new 'opt_in_rules' or deprecated 'enabled_rules' for now.

View File

@ -1,16 +1,27 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// A collection of keys and values as parsed out of SourceKit, with many conveniences for accessing SwiftLint-specific
/// values.
public struct SourceKittenDictionary { public struct SourceKittenDictionary {
/// The underlying SourceKitten dictionary.
public let value: [String: SourceKitRepresentable] public let value: [String: SourceKitRepresentable]
/// The cached substructure for this dictionary. Empty if there is no substructure.
public let substructure: [SourceKittenDictionary] public let substructure: [SourceKittenDictionary]
/// The kind of Swift expression represented by this dictionary, if it is an expression.
public let expressionKind: SwiftExpressionKind? public let expressionKind: SwiftExpressionKind?
/// The kind of Swift declaration represented by this dictionary, if it is a declaration.
public let declarationKind: SwiftDeclarationKind? public let declarationKind: SwiftDeclarationKind?
/// The kind of Swift statement represented by this dictionary, if it is a statement.
public let statementKind: StatementKind? public let statementKind: StatementKind?
/// The accessibility level for this dictionary, if it is a declaration.
public let accessibility: AccessControlLevel? public let accessibility: AccessControlLevel?
/// Creates a SourceKitten dictionary given a `Dictionary<String, SourceKitRepresentable>` input.
///
/// - parameter value: The input dictionary/
init(_ value: [String: SourceKitRepresentable]) { init(_ value: [String: SourceKitRepresentable]) {
self.value = value self.value = value
@ -91,15 +102,18 @@ public struct SourceKittenDictionary {
return (value["key.doclength"] as? Int64).flatMap({ Int($0) }) return (value["key.doclength"] as? Int64).flatMap({ Int($0) })
} }
/// The attribute for this dictionary, as returned by SourceKit.
var attribute: String? { var attribute: String? {
return value["key.attribute"] as? String return value["key.attribute"] as? String
} }
/// The `SwiftDeclarationAttributeKind` values associated with this dictionary.
var enclosedSwiftAttributes: [SwiftDeclarationAttributeKind] { var enclosedSwiftAttributes: [SwiftDeclarationAttributeKind] {
return swiftAttributes.compactMap { $0.attribute } return swiftAttributes.compactMap { $0.attribute }
.compactMap(SwiftDeclarationAttributeKind.init(rawValue:)) .compactMap(SwiftDeclarationAttributeKind.init(rawValue:))
} }
/// The fully preserved SourceKitten dictionaries for all the attributes associated with this dictionary.
var swiftAttributes: [SourceKittenDictionary] { var swiftAttributes: [SourceKittenDictionary] {
let array = value["key.attributes"] as? [SourceKitRepresentable] ?? [] let array = value["key.attributes"] as? [SourceKitRepresentable] ?? []
let dictionaries = array.compactMap { $0 as? [String: SourceKitRepresentable] } let dictionaries = array.compactMap { $0 as? [String: SourceKitRepresentable] }

View File

@ -1,8 +1,24 @@
import Foundation import Foundation
/// An interface for enumerating files that can be linted by SwiftLint.
public protocol LintableFileManager { public protocol LintableFileManager {
func filesToLint(inPath: String, rootDirectory: String?) -> [String] /// Returns all files that can be linted in the specified path. If the path is relative, it will be appended to the
func modificationDate(forFileAtPath: String) -> Date? /// 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 { extension FileManager: LintableFileManager {

View File

@ -5,17 +5,12 @@ private var regexCache = [RegexCacheKey: NSRegularExpression]()
private let regexCacheLock = NSLock() private let regexCacheLock = NSLock()
private struct RegexCacheKey: Hashable { 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 pattern: String
let options: NSRegularExpression.Options let options: NSRegularExpression.Options
// swiftlint:enable unused_declaration
}
extension NSRegularExpression.Options: Hashable { func hash(into hasher: inout Hasher) {
public func hash(into hasher: inout Hasher) { hasher.combine(pattern)
hasher.combine(rawValue) hasher.combine(options.rawValue)
} }
} }

View File

@ -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. - parameter object: Object to print.
*/ */
@ -29,7 +29,7 @@ public func queuedPrint<T>(_ 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. - 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. the source path from the compiled binary.
*/ */
public func queuedFatalError(_ string: String, file: StaticString = #file, line: UInt = #line) -> Never { public func queuedFatalError(_ string: String, file: StaticString = #file, line: UInt = #line) -> Never {

View File

@ -74,6 +74,7 @@ extension String {
return NSRange(location: 0, length: utf16.count) return NSRange(location: 0, length: utf16.count)
} }
/// Returns a new string, converting the path to a canonical absolute path.
public func absolutePathStandardized() -> String { public func absolutePathStandardized() -> String {
return bridge().absolutePathRepresentation().bridge().standardizingPath return bridge().absolutePathRepresentation().bridge().standardizingPath
} }

View File

@ -1,6 +1,6 @@
import SourceKittenFramework import SourceKittenFramework
public extension SwiftDeclarationAttributeKind { extension SwiftDeclarationAttributeKind {
enum ModifierGroup: String, CustomDebugStringConvertible { enum ModifierGroup: String, CustomDebugStringConvertible {
case `override` case `override`
case acl case acl
@ -85,7 +85,7 @@ public extension SwiftDeclarationAttributeKind {
} }
} }
public var debugDescription: String { var debugDescription: String {
return self.rawValue return self.rawValue
} }
} }

View File

@ -1,9 +1,17 @@
/// The kind of expression for a contiguous set of Swift source tokens.
public enum SwiftExpressionKind: String { public enum SwiftExpressionKind: String {
/// A call to a named function or closure.
case call = "source.lang.swift.expr.call" case call = "source.lang.swift.expr.call"
/// An argument value for a function or closure.
case argument = "source.lang.swift.expr.argument" case argument = "source.lang.swift.expr.argument"
/// An Array expression.
case array = "source.lang.swift.expr.array" case array = "source.lang.swift.expr.array"
/// A Dictionary expression.
case dictionary = "source.lang.swift.expr.dictionary" 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" 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" 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" case tuple = "source.lang.swift.expr.tuple"
} }

View File

@ -180,6 +180,7 @@ extension SwiftLintFile {
return syntaxKindsByLines return syntaxKindsByLines
} }
/// Invalidates all cached data for this file.
public func invalidateCache() { public func invalidateCache() {
responseCache.invalidate(self) responseCache.invalidate(self)
assertHandlerCache.invalidate(self) assertHandlerCache.invalidate(self)

View File

@ -1,19 +1,19 @@
struct RegexHelpers { struct RegexHelpers {
// A single variable /// A single variable
static let varName = "[a-zA-Z_][a-zA-Z0-9_]+" 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*" static let varNameGroup = "\\s*(\(varName))\\s*"
// Two variables (capturables) /// Two variables (capturables)
static let twoVars = "\(varNameGroup),\(varNameGroup)" static let twoVars = "\(varNameGroup),\(varNameGroup)"
// A number /// A number
static let number = "[\\-0-9\\.]+" static let number = "[\\-0-9\\.]+"
// A variable or a number (capturable) /// A variable or a number (capturable)
static let variableOrNumber = "\\s*(\(varName)|\(number))\\s*" static let variableOrNumber = "\\s*(\(varName)|\(number))\\s*"
// Two 'variable or number' /// Two 'variable or number'
static let twoVariableOrNumber = "\(variableOrNumber),\(variableOrNumber)" static let twoVariableOrNumber = "\(variableOrNumber),\(variableOrNumber)"
} }

View File

@ -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 { public enum AccessControlLevel: String, CustomStringConvertible {
/// Accessible by the declaration's immediate lexical scope.
case `private` = "source.lang.swift.accessibility.private" case `private` = "source.lang.swift.accessibility.private"
/// Accessible by the declaration's same file.
case `fileprivate` = "source.lang.swift.accessibility.fileprivate" 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" case `internal` = "source.lang.swift.accessibility.internal"
/// Accessible by the declaration's same program.
case `public` = "source.lang.swift.accessibility.public" 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" 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) { internal init?(description value: String) {
switch value { switch value {
case "private": self = .private 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) self.init(rawValue: value)
} }

View File

@ -29,11 +29,17 @@ private extension Scanner {
} }
#endif #endif
/// A SwiftLint-interpretable command to modify SwiftLint's behavior embedded as comments in source code.
public struct Command: Equatable { public struct Command: Equatable {
/// The action (verb) that SwiftLint should perform when interpreting this command.
public enum Action: String { public enum Action: String {
/// The rule(s) associated with this command should be enabled by the SwiftLint engine.
case enable case enable
/// The rule(s) associated with this command should be disabled by the SwiftLint engine.
case disable 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 { internal func inverse() -> Action {
switch self { switch self {
case .enable: return .disable 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 { public enum Modifier: String {
/// The command should only apply to the line preceding its definition.
case previous case previous
/// The command should only apply to the same line as its definition.
case this case this
/// The command should only apply to the line following its definition.
case next case next
} }
/// Text after this delimiter is not considered part of the rule. /// 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. /// commands to be documented in source code.
/// ///
/// swiftlint:disable:next force_try - Explanation here /// swiftlint:disable:next force_try - Explanation here
@ -63,6 +73,15 @@ public struct Command: Equatable {
/// Currently unused but parsed separate from rule identifiers /// Currently unused but parsed separate from rule identifiers
internal let trailingComment: String? 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<RuleIdentifier>, line: Int = 0, public init(action: Action, ruleIdentifiers: Set<RuleIdentifier>, line: Int = 0,
character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) { character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) {
self.action = action self.action = action
@ -73,6 +92,12 @@ public struct Command: Equatable {
self.trailingComment = trailingComment 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) { public init?(actionString: String, line: Int, character: Int) {
let scanner = Scanner(string: actionString) let scanner = Scanner(string: actionString)
_ = scanner.scanString(string: "swiftlint:") _ = 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] { internal func expand() -> [Command] {
guard let modifier = modifier else { guard let modifier = modifier else {
return [self] return [self]

View File

@ -1,25 +1,40 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// The configuration struct for SwiftLint. User-defined in the `.swiftlint.yml` file, drives the behavior of SwiftLint.
public struct Configuration: Hashable { 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 { 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]) case `default`(disabled: [String], optIn: [String])
/// Only enable the rules explicitly listed.
case whitelisted([String]) case whitelisted([String])
/// Enable all available rules.
case allEnabled case allEnabled
} }
// MARK: Properties // MARK: Properties
/// The standard file name to look for user-defined configurations.
public static let fileName = ".swiftlint.yml" public static let fileName = ".swiftlint.yml"
public let indentation: IndentationStyle // style to use when indenting /// The style to use when indenting Swift source code.
public let included: [String] // included public let indentation: IndentationStyle
public let excluded: [String] // excluded /// Included paths to lint.
public let reporter: String // reporter (xcode, json, csv, checkstyle) public let included: [String]
public let warningThreshold: Int? // warning threshold /// Excluded paths to not lint.
public private(set) var rootPath: String? // the root path to search for nested configurations public let excluded: [String]
public private(set) var configurationPath: String? // if successfully loaded from a path /// 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 let cachePath: String?
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
@ -45,13 +60,14 @@ public struct Configuration: Hashable {
// MARK: Rules Properties // 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] public let rules: [Rule]
internal let rulesMode: RulesMode internal let rulesMode: RulesMode
// MARK: Initializers // MARK: Initializers
/// Creates a `Configuration` by specifying its properties directly.
public init?(rulesMode: RulesMode = .default(disabled: [], optIn: []), public init?(rulesMode: RulesMode = .default(disabled: [], optIn: []),
included: [String] = [], included: [String] = [],
excluded: [String] = [], excluded: [String] = [],
@ -126,6 +142,7 @@ public struct Configuration: Hashable {
indentation = configuration.indentation indentation = configuration.indentation
} }
/// Creates a `Configuration` with convenience parameters.
public init(path: String = Configuration.fileName, rootPath: String? = nil, public init(path: String = Configuration.fileName, rootPath: String? = nil,
optional: Bool = true, quiet: Bool = false, enableAllRules: Bool = false, optional: Bool = true, quiet: Bool = false, enableAllRules: Bool = false,
cachePath: String? = nil, customRulesIdentifiers: [String] = []) { cachePath: String? = nil, customRulesIdentifiers: [String] = []) {

View File

@ -1,3 +1,5 @@
/// All possible configuration errors.
public enum ConfigurationError: Error { public enum ConfigurationError: Error {
/// The configuration didn't match internal expectations.
case unknownConfiguration case unknownConfiguration
} }

View File

@ -1,7 +1,11 @@
/// A value describing a SwiftLint violation that was corrected.
public struct Correction: Equatable { public struct Correction: Equatable {
/// The description of the rule for which this correction was applied.
public let ruleDescription: RuleDescription public let ruleDescription: RuleDescription
/// The location of the original violation that was corrected.
public let location: Location public let location: Location
/// The console-printable description for this correction.
public var consoleDescription: String { public var consoleDescription: String {
return "\(location) Corrected \(ruleDescription.name)" return "\(location) Corrected \(ruleDescription.name)"
} }

View File

@ -110,13 +110,21 @@ private extension Rule {
/// Represents a file that can be linted for style violations and corrections after being collected. /// Represents a file that can be linted for style violations and corrections after being collected.
public struct Linter { public struct Linter {
/// The file to lint with this linter.
public let file: SwiftLintFile public let file: SwiftLintFile
/// Whether or not this linter will be used to collect information from several files.
public var isCollecting: Bool public var isCollecting: Bool
fileprivate let rules: [Rule] fileprivate let rules: [Rule]
fileprivate let cache: LinterCache? fileprivate let cache: LinterCache?
fileprivate let configuration: Configuration fileprivate let configuration: Configuration
fileprivate let compilerArguments: [String] 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, public init(file: SwiftLintFile, configuration: Configuration = Configuration()!, cache: LinterCache? = nil,
compilerArguments: [String] = []) { compilerArguments: [String] = []) {
self.file = file 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:)`. /// A `CollectedLinter` is only created after a `Linter` has run its collection steps in `Linter.collect(into:)`.
public struct CollectedLinter { public struct CollectedLinter {
/// The file to lint with this linter.
public let file: SwiftLintFile public let file: SwiftLintFile
private let rules: [Rule] private let rules: [Rule]
private let cache: LinterCache? private let cache: LinterCache?
@ -161,10 +170,12 @@ public struct CollectedLinter {
compilerArguments = linter.compilerArguments compilerArguments = linter.compilerArguments
} }
/// Computes or retrieves style violations.
public func styleViolations(using storage: RuleStorage) -> [StyleViolation] { public func styleViolations(using storage: RuleStorage) -> [StyleViolation] {
return getStyleViolations(using: storage).0 return getStyleViolations(using: storage).0
} }
/// Computes or retrieves style violations and the time spent executing each rule.
public func styleViolationsAndRuleTimes(using storage: RuleStorage) public func styleViolationsAndRuleTimes(using storage: RuleStorage)
-> ([StyleViolation], [(id: String, time: Double)]) { -> ([StyleViolation], [(id: String, time: Double)]) {
return getStyleViolations(using: storage, benchmark: true) return getStyleViolations(using: storage, benchmark: true)
@ -234,6 +245,7 @@ public struct CollectedLinter {
return (cachedViolations, ruleTimes) return (cachedViolations, ruleTimes)
} }
/// Applies corrections for all rules to this file, returning performed corrections.
public func correct(using storage: RuleStorage) -> [Correction] { public func correct(using storage: RuleStorage) -> [Correction] {
if let violations = cachedStyleViolations()?.0, violations.isEmpty { if let violations = cachedStyleViolations()?.0, violations.isEmpty {
return [] return []
@ -250,6 +262,10 @@ public struct CollectedLinter {
return corrections 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) { public func format(useTabs: Bool, indentWidth: Int) {
let formattedContents = try? file.file.format(trimmingTrailingWhitespace: true, let formattedContents = try? file.file.format(trimmingTrailingWhitespace: true,
useTabs: useTabs, useTabs: useTabs,

View File

@ -16,6 +16,7 @@ private struct FileCache: Codable {
static var empty: FileCache { return FileCache(entries: [:]) } static var empty: FileCache { return FileCache(entries: [:]) }
} }
/// A persisted cache for storing and retrieving linter results.
public final class LinterCache { public final class LinterCache {
#if canImport(Darwin) || compiler(>=5.1.0) #if canImport(Darwin) || compiler(>=5.1.0)
private typealias Encoder = PropertyListEncoder private typealias Encoder = PropertyListEncoder
@ -44,6 +45,10 @@ public final class LinterCache {
self.swiftVersion = swiftVersion 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) { public init(configuration: Configuration, fileManager: LintableFileManager = FileManager.default) {
location = configuration.cacheURL location = configuration.cacheURL
lazyReadCache = Cache() lazyReadCache = Cache()
@ -85,6 +90,7 @@ public final class LinterCache {
return entry.violations return entry.violations
} }
/// Persists the cache to disk.
public func save() throws { public func save() throws {
guard let url = location else { guard let url = location else {
throw LinterCacheError.noLocation throw LinterCacheError.noLocation

View File

@ -1,10 +1,16 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// The placement of a segment of Swift in a collection of source files.
public struct Location: CustomStringConvertible, Comparable, Codable { public struct Location: CustomStringConvertible, Comparable, Codable {
/// The file path on disk for this location.
public let file: String? public let file: String?
/// The line offset in the file for this location. 1-indexed.
public let line: Int? public let line: Int?
/// The character offset in the file for this location. 1-indexed.
public let character: Int? public let character: Int?
/// A lossless printable description of this location.
public var description: String { public var description: String {
// Xcode likes warnings and errors in the following format: // Xcode likes warnings and errors in the following format:
// {full_path_to_file}{:line}{:character}: {error,warning}: {content} // {full_path_to_file}{:line}{:character}: {error,warning}: {content}
@ -13,16 +19,28 @@ public struct Location: CustomStringConvertible, Comparable, Codable {
let charString: String = ":\(character ?? 1)" let charString: String = ":\(character ?? 1)"
return [fileString, lineString, charString].joined() return [fileString, lineString, charString].joined()
} }
/// The file path for this location relative to the current working directory.
public var relativeFile: String? { public var relativeFile: String? {
return file?.replacingOccurrences(of: FileManager.default.currentDirectoryPath + "/", with: "") 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) { public init(file: String?, line: Int? = nil, character: Int? = nil) {
self.file = file self.file = file
self.line = line self.line = line
self.character = character 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) { public init(file: SwiftLintFile, byteOffset offset: Int) {
self.file = file.path self.file = file.path
if let lineAndCharacter = file.stringView.lineAndCharacter(forByteOffset: offset) { 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) { public init(file: SwiftLintFile, characterOffset offset: Int) {
self.file = file.path self.file = file.path
if let lineAndCharacter = file.stringView.lineAndCharacter(forCharacterOffset: offset) { if let lineAndCharacter = file.stringView.lineAndCharacter(forCharacterOffset: offset) {

View File

@ -1,6 +1,7 @@
// Generated using Sourcery 0.17.0 https://github.com/krzysztofzablocki/Sourcery // Generated using Sourcery 0.17.0 https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT // DO NOT EDIT
/// The rule list containing all available rules built into SwiftLint.
public let masterRuleList = RuleList(rules: [ public let masterRuleList = RuleList(rules: [
AnyObjectProtocolRule.self, AnyObjectProtocolRule.self,
ArrayInitRule.self, ArrayInitRule.self,

View File

@ -1,22 +1,42 @@
/// A contiguous region of Swift source code.
public struct Region: Equatable { 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 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 public let end: Location
/// All SwiftLint rule identifiers that are disabled in this region.
public let disabledRuleIdentifiers: Set<RuleIdentifier> public let disabledRuleIdentifiers: Set<RuleIdentifier>
/// 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<RuleIdentifier>) { public init(start: Location, end: Location, disabledRuleIdentifiers: Set<RuleIdentifier>) {
self.start = start self.start = start
self.end = end self.end = end
self.disabledRuleIdentifiers = disabledRuleIdentifiers 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 { public func contains(_ location: Location) -> Bool {
return start <= location && end >= location 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 { public func isRuleEnabled(_ rule: Rule) -> Bool {
return !isRuleDisabled(rule) 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 { public func isRuleDisabled(_ rule: Rule) -> Bool {
guard !disabledRuleIdentifiers.contains(.all) else { guard !disabledRuleIdentifiers.contains(.all) else {
return true return true
@ -27,6 +47,10 @@ public struct Region: Equatable {
return !regionIdentifiers.isDisjoint(with: identifiersToCheck) 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<String> { public func deprecatedAliasesDisabling(rule: Rule) -> Set<String> {
let identifiers = type(of: rule).description.deprecatedAliases let identifiers = type(of: rule).description.deprecatedAliases
return Set(disabledRuleIdentifiers.map { $0.stringRepresentation }).intersection(identifiers) return Set(disabledRuleIdentifiers.map { $0.stringRepresentation }).intersection(identifiers)

View File

@ -1,21 +1,65 @@
/// A detailed description for a SwiftLint rule. Used for both documentation and testing purposes.
public struct RuleDescription: Equatable, Codable { 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 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 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 public let description: String
/// The `RuleKind` that best categorizes this rule.
public let kind: RuleKind 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] 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] 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] public let corrections: [String: String]
/// Any previous iteration of the rule's identifier that was previously shipped with SwiftLint.
public let deprecatedAliases: Set<String> public let deprecatedAliases: Set<String>
/// The oldest version of the Swift compiler supported by this rule.
public let minSwiftVersion: SwiftVersion 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 public let requiresFileOnDisk: Bool
/// The console-printable string for this description.
public var consoleDescription: String { return "\(name) (\(identifier)): \(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] { public var allIdentifiers: [String] {
return Array(deprecatedAliases) + [identifier] return Array(deprecatedAliases) + [identifier]
} }
/// Creates a `RuleDescription` by specifying all its properties directly.
public init(identifier: String, name: String, description: String, kind: RuleKind, public init(identifier: String, name: String, description: String, kind: RuleKind,
minSwiftVersion: SwiftVersion = .three, minSwiftVersion: SwiftVersion = .three,
nonTriggeringExamples: [String] = [], triggeringExamples: [String] = [], nonTriggeringExamples: [String] = [], triggeringExamples: [String] = [],

View File

@ -1,9 +1,20 @@
/// An identifier representing a SwiftLint rule, or all rules.
public enum RuleIdentifier: Hashable, ExpressibleByStringLiteral { 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 case all
/// Represents a single SwiftLint rule with the specified identifier.
case single(identifier: String) case single(identifier: String)
// MARK: - Properties
private static let allStringRepresentation = "all" private static let allStringRepresentation = "all"
/// The spelling of the string for this idenfitier.
public var stringRepresentation: String { public var stringRepresentation: String {
switch self { switch self {
case .all: 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) { public init(_ value: String) {
self = value == RuleIdentifier.allStringRepresentation ? .all : .single(identifier: value) self = value == RuleIdentifier.allStringRepresentation ? .all : .single(identifier: value)
} }
// MARK: - ExpressibleByStringLiteral Conformance
public init(stringLiteral value: String) { public init(stringLiteral value: String) {
self = RuleIdentifier(value) self = RuleIdentifier(value)
} }

View File

@ -1,7 +1,13 @@
/// All the possible rule kinds (categories).
public enum RuleKind: String, Codable { public enum RuleKind: String, Codable {
/// Describes rules that validate Swift source conventions.
case lint case lint
/// Describes rules that validate common practices in the Swift community.
case idiomatic case idiomatic
/// Describes rules that validate stylistic choices.
case style case style
/// Describes rules that validate magnitudes or measurements of Swift source.
case metrics case metrics
/// Describes rules that validate that code patterns with poor performance are avoided.
case performance case performance
} }

View File

@ -1,15 +1,27 @@
/// All possible configuration errors.
public enum RuleListError: Error { public enum RuleListError: Error {
/// The rule list contains more than one configuration for the specified rule.
case duplicatedConfigurations(rule: Rule.Type) case duplicatedConfigurations(rule: Rule.Type)
} }
/// A list of available SwiftLint rules.
public struct RuleList { public struct RuleList {
/// The rules contained in this list.
public let list: [String: Rule.Type] public let list: [String: Rule.Type]
private let aliases: [String: String] 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...) { public init(rules: Rule.Type...) {
self.init(rules: rules) 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]) { public init(rules: [Rule.Type]) {
var tmpList = [String: Rule.Type]() var tmpList = [String: Rule.Type]()
var tmpAliases = [String: String]() var tmpAliases = [String: String]()
@ -26,6 +38,8 @@ public struct RuleList {
aliases = tmpAliases aliases = tmpAliases
} }
// MARK: - Internal
internal func configuredRules(with dictionary: [String: Any]) throws -> [Rule] { internal func configuredRules(with dictionary: [String: Any]) throws -> [Rule] {
var rules = [String: Rule]() var rules = [String: Rule]()

View File

@ -1,7 +1,14 @@
/// A configuration parameter for rules.
public struct RuleParameter<T: Equatable>: Equatable { public struct RuleParameter<T: Equatable>: Equatable {
/// The severity that should be assigned to the violation of this parameter's value is met.
public let severity: ViolationSeverity public let severity: ViolationSeverity
/// The value to configure the rule.
public let value: T 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) { public init(severity: ViolationSeverity, value: T) {
self.severity = severity self.severity = severity
self.value = value self.value = value

View File

@ -1,13 +1,20 @@
import Dispatch import Dispatch
/// A storage mechanism for aggregating the results of `CollectingRule`s.
public class RuleStorage { public class RuleStorage {
private var storage: [ObjectIdentifier: [SwiftLintFile: Any]] private var storage: [ObjectIdentifier: [SwiftLintFile: Any]]
private let access = DispatchQueue(label: "io.realm.swiftlint.ruleStorageAccess", attributes: .concurrent) private let access = DispatchQueue(label: "io.realm.swiftlint.ruleStorageAccess", attributes: .concurrent)
/// Creates a `RuleStorage` with no initial stored data.
public init() { public init() {
storage = [:] 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<R: CollectingRule>(info: R.FileInfo, for file: SwiftLintFile, in rule: R) { func collect<R: CollectingRule>(info: R.FileInfo, for file: SwiftLintFile, in rule: R) {
let key = ObjectIdentifier(R.self) let key = ObjectIdentifier(R.self)
access.sync(flags: .barrier) { 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<R: CollectingRule>(for rule: R) -> [SwiftLintFile: R.FileInfo]? { func collectedInfo<R: CollectingRule>(for rule: R) -> [SwiftLintFile: R.FileInfo]? {
return access.sync { return access.sync {
storage[ObjectIdentifier(R.self)] as? [SwiftLintFile: R.FileInfo] storage[ObjectIdentifier(R.self)] as? [SwiftLintFile: R.FileInfo]

View File

@ -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 { public struct StyleViolation: CustomStringConvertible, Equatable, Codable {
/// The description of the rule that generated this violation.
public let ruleDescription: RuleDescription public let ruleDescription: RuleDescription
/// The severity of this violation.
public let severity: ViolationSeverity public let severity: ViolationSeverity
/// The location of this violation.
public let location: Location public let location: Location
/// The justification for this violation.
public let reason: String public let reason: String
/// A printable description for this violation.
public var description: String { public var description: String {
return XcodeReporter.generateForSingleViolation(self) 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, public init(ruleDescription: RuleDescription, severity: ViolationSeverity = .warning,
location: Location, reason: String? = nil) { location: Location, reason: String? = nil) {
self.ruleDescription = ruleDescription self.ruleDescription = ruleDescription

View File

@ -1,11 +1,12 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// A unit of Swift source code, either on disk or in memory.
public final class SwiftLintFile { public final class SwiftLintFile {
private static var id = 0 private static var id = 0
private static var lock = NSLock() private static var lock = NSLock()
private static func nextId () -> Int { private static func nextID () -> Int {
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
id += 1 id += 1
@ -15,41 +16,61 @@ public final class SwiftLintFile {
let file: File let file: File
let id: Int let id: Int
/// Creates a `SwiftLintFile` with a SourceKitten `File`.
///
/// - parameter file: A file from SourceKitten.
public init(file: File) { public init(file: File) {
self.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) { public convenience init?(path: String) {
guard let file = File(path: path) else { return nil } guard let file = File(path: path) else { return nil }
self.init(file: file) 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) { public convenience init(pathDeferringReading path: String) {
self.init(file: File(pathDeferringReading: path)) 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) { public convenience init(contents: String) {
self.init(file: File(contents: contents)) self.init(file: File(contents: contents))
} }
/// The path on disk for this file.
public var path: String? { public var path: String? {
return file.path return file.path
} }
/// The file's contents.
public var contents: String { public var contents: String {
return file.contents return file.contents
} }
/// A string view into the contents of this file optimized for string manipulation operations.
public var stringView: StringView { public var stringView: StringView {
return file.stringView return file.stringView
} }
/// The parsed lines for this file's contents.
public var lines: [Line] { public var lines: [Line] {
return file.lines return file.lines
} }
} }
// MARK: - Hashable Conformance
extension SwiftLintFile: Hashable { extension SwiftLintFile: Hashable {
public static func == (lhs: SwiftLintFile, rhs: SwiftLintFile) -> Bool { public static func == (lhs: SwiftLintFile, rhs: SwiftLintFile) -> Bool {
return lhs.id == rhs.id return lhs.id == rhs.id

View File

@ -1,18 +1,25 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// Represents a Swift file's syntax information.
public struct SwiftLintSyntaxMap { public struct SwiftLintSyntaxMap {
/// The raw `SyntaxMap` obtained by SourceKitten.
public let value: SyntaxMap public let value: SyntaxMap
/// The SwiftLint-specific syntax tokens for this syntax map.
public let tokens: [SwiftLintSyntaxToken] 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) { public init(value: SyntaxMap) {
self.value = value self.value = value
self.tokens = value.tokens.map(SwiftLintSyntaxToken.init) 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] { internal func tokens(inByteRange byteRange: NSRange) -> [SwiftLintSyntaxToken] {
func intersect(_ token: SwiftLintSyntaxToken) -> Bool { func intersect(_ token: SwiftLintSyntaxToken) -> Bool {
return token.range.intersects(byteRange) return token.range.intersects(byteRange)
@ -35,6 +42,9 @@ public struct SwiftLintSyntaxMap {
return Array(tokensAfterFirstIntersection) return Array(tokensAfterFirstIntersection)
} }
/// Returns the syntax kinds in the specified byte range.
///
/// - parameter byteRange: Byte-based NSRange.
internal func kinds(inByteRange byteRange: NSRange) -> [SyntaxKind] { internal func kinds(inByteRange byteRange: NSRange) -> [SyntaxKind] {
return tokens(inByteRange: byteRange).compactMap { $0.kind } return tokens(inByteRange: byteRange).compactMap { $0.kind }
} }

View File

@ -1,23 +1,33 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// A SwiftLint-aware Swift syntax token.
public struct SwiftLintSyntaxToken { public struct SwiftLintSyntaxToken {
/// The raw `SyntaxToken` obtained by SourceKitten.
public let value: SyntaxToken public let value: SyntaxToken
/// The syntax kind associated with is token.
public let kind: SyntaxKind? 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) { public init(value: SyntaxToken) {
self.value = value self.value = value
kind = SyntaxKind(rawValue: value.type) kind = SyntaxKind(rawValue: value.type)
} }
/// The byte range in a source file for this token.
public var range: NSRange { public var range: NSRange {
return NSRange(location: value.offset, length: value.length) return NSRange(location: value.offset, length: value.length)
} }
/// The starting byte offset in a source file for this token.
public var offset: Int { public var offset: Int {
return value.offset return value.offset
} }
/// The length in bytes for this token.
public var length: Int { public var length: Int {
return value.length return value.length
} }

View File

@ -1,7 +1,8 @@
import Foundation import Foundation
import SourceKittenFramework 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 typealias RawValue = String
public let rawValue: String public let rawValue: String
@ -9,23 +10,29 @@ public struct SwiftVersion: RawRepresentable, Codable {
public init(rawValue: String) { public init(rawValue: String) {
self.rawValue = rawValue self.rawValue = rawValue
} }
}
extension SwiftVersion: Comparable {
// Comparable
public static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool { public static func < (lhs: SwiftVersion, rhs: SwiftVersion) -> Bool {
return lhs.rawValue < rhs.rawValue return lhs.rawValue < rhs.rawValue
} }
} }
public extension SwiftVersion { public extension SwiftVersion {
/// Swift 3.x - https://swift.org/download/#swift-30
static let three = SwiftVersion(rawValue: "3.0.0") 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") 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") 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") 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") 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") 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 = { static let current: SwiftVersion = {
// Allow forcing the Swift version, useful in cases where SourceKit isn't available // Allow forcing the Swift version, useful in cases where SourceKit isn't available
if let envVersion = ProcessInfo.processInfo.environment["SWIFTLINT_SWIFT_VERSION"] { if let envVersion = ProcessInfo.processInfo.environment["SWIFTLINT_SWIFT_VERSION"] {

View File

@ -1,5 +1,8 @@
/// A type describing the SwiftLint version.
public struct Version { public struct Version {
/// The string value for this version.
public let value: String public let value: String
/// The current SwiftLint version.
public static let current = Version(value: "0.38.1") public static let current = Version(value: "0.38.1")
} }

View File

@ -1,5 +1,8 @@
/// The magnitude of a `StyleViolation`.
public enum ViolationSeverity: String, Comparable, Codable { 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 case warning
/// Fatal. If using SwiftLint as an Xcode build phase, Xcode will mark the build as having failed.
case error case error
// MARK: Comparable // MARK: Comparable

View File

@ -9,7 +9,12 @@ internal enum YamlParserError: Error, Equatable {
// MARK: - YamlParser // MARK: - YamlParser
/// An interface for parsing YAML.
public struct YamlParser { 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, public static func parse(_ yaml: String,
env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] { env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] {
do { do {

View File

@ -1,8 +1,20 @@
import SourceKittenFramework 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 { public protocol ASTRule: Rule {
/// The kind of token being recursed over.
associatedtype KindType: RawRepresentable 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] func validate(file: SwiftLintFile, kind: KindType, dictionary: SourceKittenDictionary) -> [StyleViolation]
/// Get the `kind` from the specified dictionary.
func kind(from dictionary: SourceKittenDictionary) -> KindType? func kind(from dictionary: SourceKittenDictionary) -> KindType?
} }
@ -11,6 +23,11 @@ public extension ASTRule {
return validate(file: file, dictionary: file.structureDictionary) 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] { func validate(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [StyleViolation] {
return dictionary.traverseDepthFirst { subDict in return dictionary.traverseDepthFirst { subDict in
guard let kind = self.kind(from: subDict) else { return nil } guard let kind = self.kind(from: subDict) else { return nil }

View File

@ -1,10 +1,24 @@
/// An interface for reporting violations as strings.
public protocol Reporter: CustomStringConvertible { public protocol Reporter: CustomStringConvertible {
/// The unique identifier for this reporter.
static var identifier: String { get } 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 } 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 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 public func reporterFrom(identifier: String) -> Reporter.Type { // swiftlint:disable:this cyclomatic_complexity
switch identifier { switch identifier {
case XcodeReporter.identifier: case XcodeReporter.identifier:

View File

@ -1,19 +1,53 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// An executable value that can identify issues (violations) in Swift source code.
public protocol Rule { public protocol Rule {
/// A verbose description of many of this rule's properties.
static var description: RuleDescription { get } static var description: RuleDescription { get }
/// A description of how this rule has been configured to run.
var configurationDescription: String { get } 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 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] 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] func validate(file: SwiftLintFile) -> [StyleViolation]
/// Whether or not the specified rule is equivalent to the current rule.
func isEqualTo(_ rule: Rule) -> Bool 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]) 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] 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 {} public protocol OptInRule: Rule {}
/// A rule that has unit tests automatically generated using Sourcery.
public protocol AutomaticTestableRule: Rule {} public protocol AutomaticTestableRule: Rule {}
/// A rule that is user-configurable.
public protocol ConfigurationProviderRule: Rule { public protocol ConfigurationProviderRule: Rule {
/// The type of configuration used to configure this rule.
associatedtype ConfigurationType: RuleConfiguration associatedtype ConfigurationType: RuleConfiguration
/// This rule's configuration.
var configuration: ConfigurationType { get set } var configuration: ConfigurationType { get set }
} }
/// A rule that can correct violations.
public protocol CorrectableRule: Rule { 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] 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] 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] 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 { public protocol SubstitutionCorrectableRule: CorrectableRule {
/// Returns the NSString-based `NSRange`s to be replaced in the specified file.
func violationRanges(in file: SwiftLintFile) -> [NSRange] 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)? 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 { 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, func violationRanges(in file: SwiftLintFile, kind: KindType,
dictionary: SourceKittenDictionary) -> [NSRange] 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 {} 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 protocol AnalyzerRule: OptInRule {}
public extension AnalyzerRule { public extension AnalyzerRule {
@ -118,6 +187,7 @@ public extension AnalyzerRule {
} }
} }
/// :nodoc:
public extension AnalyzerRule where Self: CorrectableRule { public extension AnalyzerRule where Self: CorrectableRule {
func correct(file: SwiftLintFile) -> [Correction] { func correct(file: SwiftLintFile) -> [Correction] {
queuedFatalError("Must call `correct(file:compilerArguments:)` for AnalyzerRule") 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. /// Type-erased protocol used to check whether a rule is collectable.
public protocol AnyCollectingRule: Rule { } public protocol AnyCollectingRule: Rule { }
/// A rule that requires knowledge of all other files being linted.
public protocol CollectingRule: AnyCollectingRule { public protocol CollectingRule: AnyCollectingRule {
/// The kind of information to collect for each file being linted for this rule.
associatedtype FileInfo 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 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 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], func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo],
compilerArguments: [String]) -> [StyleViolation] 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] 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 { 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], func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo],
compilerArguments: [String]) -> [Correction] 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] func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [Correction]
} }
@ -239,6 +350,7 @@ public extension ConfigurationProviderRule {
// MARK: - == Implementations // MARK: - == Implementations
/// :nodoc:
public extension Array where Element == Rule { public extension Array where Element == Rule {
static func == (lhs: Array, rhs: Array) -> Bool { static func == (lhs: Array, rhs: Array) -> Bool {
if lhs.count != rhs.count { return false } if lhs.count != rhs.count { return false }

View File

@ -1,7 +1,14 @@
/// A configuration value for a rule to allow users to modify its behavior.
public protocol RuleConfiguration { public protocol RuleConfiguration {
/// A human-readable description for this configuration and its applied values.
var consoleDescription: String { get } 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 mutating func apply(configuration: Any) throws
/// Whether the specified configuration is equivalent to the current value.
func isEqualTo(_ ruleConfiguration: RuleConfiguration) -> Bool func isEqualTo(_ ruleConfiguration: RuleConfiguration) -> Bool
} }

View File

@ -1,16 +1,9 @@
import Foundation import Foundation
private extension String { /// Reports violations as a newline-separated string of comma-separated values (CSV).
func escapedForCSV() -> String {
let escapedString = replacingOccurrences(of: "\"", with: "\"\"")
if escapedString.contains(",") || escapedString.contains("\n") {
return "\"\(escapedString)\""
}
return escapedString
}
}
public struct CSVReporter: Reporter { public struct CSVReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "csv" public static let identifier = "csv"
public static let isRealtime = false public static let isRealtime = false
@ -33,7 +26,9 @@ public struct CSVReporter: Reporter {
return rows.joined(separator: "\n") return rows.joined(separator: "\n")
} }
fileprivate static func csvRow(for violation: StyleViolation) -> String { // MARK: - Private
private static func csvRow(for violation: StyleViolation) -> String {
return [ return [
violation.location.file?.escapedForCSV() ?? "", violation.location.file?.escapedForCSV() ?? "",
violation.location.line?.description ?? "", violation.location.line?.description ?? "",
@ -45,3 +40,13 @@ public struct CSVReporter: Reporter {
].joined(separator: ",") ].joined(separator: ",")
} }
} }
private extension String {
func escapedForCSV() -> String {
let escapedString = replacingOccurrences(of: "\"", with: "\"\"")
if escapedString.contains(",") || escapedString.contains("\n") {
return "\"\(escapedString)\""
}
return escapedString
}
}

View File

@ -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 { public struct CheckstyleReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "checkstyle" public static let identifier = "checkstyle"
public static let isRealtime = false public static let isRealtime = false
@ -17,6 +21,8 @@ public struct CheckstyleReporter: Reporter {
].joined() ].joined()
} }
// MARK: - Private
private static func generateForViolationFile(_ file: String, violations: [StyleViolation]) -> String { private static func generateForViolationFile(_ file: String, violations: [StyleViolation]) -> String {
return [ return [
"\n\t<file name=\"", file, "\">\n", "\n\t<file name=\"", file, "\">\n",

View File

@ -1,4 +1,7 @@
/// Reports violations in the format that's both fun and easy to read.
public struct EmojiReporter: Reporter { public struct EmojiReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "emoji" public static let identifier = "emoji"
public static let isRealtime = false public static let isRealtime = false
@ -13,6 +16,8 @@ public struct EmojiReporter: Reporter {
.map(report).joined(separator: "\n") .map(report).joined(separator: "\n")
} }
// MARK: - Private
private static func report(for file: String, with violations: [StyleViolation]) -> String { private static func report(for file: String, with violations: [StyleViolation]) -> String {
let lines = [file] + violations.sorted { lhs, rhs in let lines = [file] + violations.sorted { lhs, rhs in
guard lhs.severity == rhs.severity else { guard lhs.severity == rhs.severity else {

View File

@ -1,4 +1,7 @@
/// Reports violations in the format GitHub-hosted virtual machine for Actions can recognize as messages.
public struct GitHubActionsLoggingReporter: Reporter { public struct GitHubActionsLoggingReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "github-actions-logging" public static let identifier = "github-actions-logging"
public static let isRealtime = true public static let isRealtime = true
@ -10,7 +13,9 @@ public struct GitHubActionsLoggingReporter: Reporter {
return violations.map(generateForSingleViolation).joined(separator: "\n") 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 // swiftlint:disable:next line_length
// https://help.github.com/en/github/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands // 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} // ::(warning|error) file={relative_path_to_file},line={:line},col={:character}::{content}

View File

@ -6,12 +6,15 @@ private let formatter: DateFormatter = {
return formatter return formatter
}() }()
/// Reports violations as HTML.
public struct HTMLReporter: Reporter { public struct HTMLReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "html" public static let identifier = "html"
public static let isRealtime = false public static let isRealtime = false
public var description: String { public var description: String {
return "Reports violations as HTML" return "Reports violations as HTML."
} }
public static func generateReport(_ violations: [StyleViolation]) -> String { public static func generateReport(_ violations: [StyleViolation]) -> String {
@ -19,6 +22,8 @@ public struct HTMLReporter: Reporter {
dateString: formatter.string(from: Date())) dateString: formatter.string(from: Date()))
} }
// MARK: - Internal
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length
internal static func generateReport(_ violations: [StyleViolation], swiftlintVersion: String, internal static func generateReport(_ violations: [StyleViolation], swiftlintVersion: String,
dateString: String) -> String { dateString: String) -> String {
@ -143,6 +148,8 @@ public struct HTMLReporter: Reporter {
].joined() ].joined()
} }
// MARK: - Private
private static func generateSingleRow(for violation: StyleViolation, at index: Int) -> String { private static func generateSingleRow(for violation: StyleViolation, at index: Int) -> String {
let severity: String = violation.severity.rawValue.capitalized let severity: String = violation.severity.rawValue.capitalized
let location = violation.location let location = violation.location

View File

@ -1,7 +1,10 @@
import Foundation import Foundation
import SourceKittenFramework import SourceKittenFramework
/// Reports violations as a JSON array.
public struct JSONReporter: Reporter { public struct JSONReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "json" public static let identifier = "json"
public static let isRealtime = false public static let isRealtime = false
@ -13,7 +16,9 @@ public struct JSONReporter: Reporter {
return toJSON(violations.map(dictionary(for:))) 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 [ return [
"file": violation.location.file ?? NSNull() as Any, "file": violation.location.file ?? NSNull() as Any,
"line": violation.location.line ?? NSNull() as Any, "line": violation.location.line ?? NSNull() as Any,

View File

@ -1,4 +1,7 @@
/// Reports violations as JUnit XML.
public struct JUnitReporter: Reporter { public struct JUnitReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "junit" public static let identifier = "junit"
public static let isRealtime = false public static let isRealtime = false

View File

@ -1,21 +1,14 @@
import Foundation import Foundation
private extension String { /// Reports violations as markdown formated (with tables).
func escapedForMarkdown() -> String {
let escapedString = replacingOccurrences(of: "\"", with: "\"\"")
if escapedString.contains("|") || escapedString.contains("\n") {
return "\"\(escapedString)\""
}
return escapedString
}
}
public struct MarkdownReporter: Reporter { public struct MarkdownReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "markdown" public static let identifier = "markdown"
public static let isRealtime = false public static let isRealtime = false
public var description: String { 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 { public static func generateReport(_ violations: [StyleViolation]) -> String {
@ -31,7 +24,9 @@ public struct MarkdownReporter: Reporter {
return rows.joined(separator: "\n") return rows.joined(separator: "\n")
} }
fileprivate static func markdownRow(for violation: StyleViolation) -> String { // MARK: - Private
private static func markdownRow(for violation: StyleViolation) -> String {
return [ return [
violation.location.file?.escapedForMarkdown() ?? "", violation.location.file?.escapedForMarkdown() ?? "",
violation.location.line?.description ?? "", violation.location.line?.description ?? "",
@ -41,7 +36,7 @@ public struct MarkdownReporter: Reporter {
].joined(separator: " | ") ].joined(separator: " | ")
} }
fileprivate static func severity(for severity: ViolationSeverity) -> String { private static func severity(for severity: ViolationSeverity) -> String {
switch severity { switch severity {
case .error: case .error:
return ":stop\\_sign:" 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
}
}

View File

@ -1,6 +1,9 @@
import SourceKittenFramework import SourceKittenFramework
/// Reports violations in SonarQube import format.
public struct SonarQubeReporter: Reporter { public struct SonarQubeReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "sonarqube" public static let identifier = "sonarqube"
public static let isRealtime = false public static let isRealtime = false
@ -12,6 +15,8 @@ public struct SonarQubeReporter: Reporter {
return toJSON(["issues": violations.map(dictionary(for:))]) return toJSON(["issues": violations.map(dictionary(for:))])
} }
// MARK: - Private
// refer to https://docs.sonarqube.org/display/SONAR/Generic+Issue+Data // refer to https://docs.sonarqube.org/display/SONAR/Generic+Issue+Data
private static func dictionary(for violation: StyleViolation) -> [String: Any] { private static func dictionary(for violation: StyleViolation) -> [String: Any] {
return [ return [

View File

@ -1,4 +1,7 @@
/// Reports violations in the format Xcode uses to display in the IDE. (default)
public struct XcodeReporter: Reporter { public struct XcodeReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "xcode" public static let identifier = "xcode"
public static let isRealtime = true public static let isRealtime = true
@ -10,6 +13,9 @@ public struct XcodeReporter: Reporter {
return violations.map(generateForSingleViolation).joined(separator: "\n") 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 { internal static func generateForSingleViolation(_ violation: StyleViolation) -> String {
// {full_path_to_file}{:line}{:character}: {error,warning}: {content} // {full_path_to_file}{:line}{:character}: {error,warning}: {content}
return [ return [

View File

@ -8,7 +8,11 @@ public struct ModifierOrderConfiguration: RuleConfiguration, Equatable {
return severityConfiguration.consoleDescription + ", preferred_modifier_order: \(preferredModifierOrder)" return severityConfiguration.consoleDescription + ", preferred_modifier_order: \(preferredModifierOrder)"
} }
public init(preferredModifierOrder: [SwiftDeclarationAttributeKind.ModifierGroup] = []) { public init() {
self.preferredModifierOrder = []
}
init(preferredModifierOrder: [SwiftDeclarationAttributeKind.ModifierGroup] = []) {
self.preferredModifierOrder = preferredModifierOrder self.preferredModifierOrder = preferredModifierOrder
} }

View File

@ -106,17 +106,23 @@ jobs:
- job: jazzy - job: jazzy
pool: pool:
vmImage: 'Ubuntu 16.04' vmImage: 'macOS 10.14'
container: norionomura/jazzy:0.13.0_swift-5.1.3 variables:
DEVELOPER_DIR: /Applications/Xcode_11.3.app
steps: steps:
- script: swift run swiftlint generate-docs - script: swift run swiftlint generate-docs
displayName: Run swiftlint generate-docs displayName: Run swiftlint generate-docs
- script: bundle install --path vendor/bundle - script: bundle install --path vendor/bundle
displayName: bundle install displayName: bundle install
- script: sourcekitten doc --spm-module SwiftLintFramework > docs.json - script: bundle exec jazzy
displayName: Generate documentation json
- script: bundle exec jazzy --sourcekitten-sourcefile docs.json
displayName: Run 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 - task: PublishPipelineArtifact@0
inputs: inputs:
artifactName: 'API Docs' artifactName: 'API Docs'