Compare commits

...

10 Commits

Author SHA1 Message Date
JP Simard 5257b61fe9
Fix changelog formatting 2018-08-12 14:56:41 -07:00
JP Simard 5d91e3a73c
Update docs 2018-08-12 14:56:12 -07:00
JP Simard 5b53a34055
Add changelog entries 2018-08-12 14:39:27 -07:00
JP Simard 1f0a28944a
Update docs 2018-08-12 14:39:27 -07:00
JP Simard cfdf76e4dd
Test compiler argument rules 2018-08-12 14:39:27 -07:00
JP Simard 3e6bea7964
Update Rules.md 2018-08-12 14:39:27 -07:00
JP Simard 142c5c3304
Add ExplicitSelfRule 2018-08-12 14:39:27 -07:00
JP Simard 132a395c76
Add CompilerArgumentRule 2018-08-12 14:39:27 -07:00
JP Simard bd54b861ae
Add compiler log path arguments to lint/autocorrect 2018-08-12 14:39:27 -07:00
JP Simard aee843e1b4
Add LintableFilesVisitor 2018-08-12 14:39:27 -07:00
17 changed files with 783 additions and 179 deletions

View File

@ -15,6 +15,19 @@
[Ornithologist Coder](https://github.com/ornithocoder)
[#52](https://github.com/realm/SwiftLint/issues/52)
* [Experimental] Add a new `CompilerArgumentRule` type which can lint Swift
files using the full type-checked AST. Rules of this type will be added over
time. The compiler log path containing the clean `swiftc` build command
invocation (incremental builds will fail) must be passed to `lint` or
`autocorrect` via the `--compiler-log-path` flag.
e.g. `--compiler-log-path /path/to/xcodebuild.log`
[JP Simard](https://github.com/jpsim)
* [Experimental] Add a new opt-in `explicit_self` rule to enforce the use of
explicit references to `self.` when accessing instance variables or
functions.
[JP Simard](https://github.com/jpsim)
#### Bug Fixes
* Fix `comma` rule false positives on object literals (for example, images).

View File

@ -35,6 +35,7 @@
* [Explicit ACL](#explicit-acl)
* [Explicit Enum Raw Value](#explicit-enum-raw-value)
* [Explicit Init](#explicit-init)
* [Explicit Self](#explicit-self)
* [Explicit Top Level ACL](#explicit-top-level-acl)
* [Explicit Type Interface](#explicit-type-interface)
* [Extension Access Modifier](#extension-access-modifier)
@ -5341,6 +5342,63 @@ func foo() -> [String] {
## Explicit Self
Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version
--- | --- | --- | --- | ---
`explicit_self` | Disabled | Yes | style | 3.0.0
Instance variables and functions should be explicitly accessed with 'self.'.
### Examples
<details>
<summary>Non Triggering Examples</summary>
```swift
struct A {
func f1() {}
func f2() {
self.f1()
}
}
```
```swift
struct A {
let p1: Int
func f1() {
_ = self.p1
}
}
```
</details>
<details>
<summary>Triggering Examples</summary>
```swift
struct A {
func f1() {}
func f2() {
↓f1()
}
}
```
```swift
struct A {
let p1: Int
func f1() {
_ = ↓p1
}
}
```
</details>
## Explicit Top Level ACL
Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version

View File

@ -46,7 +46,8 @@ private extension Rule {
}
func lint(file: File, regions: [Region], benchmark: Bool,
superfluousDisableCommandRule: SuperfluousDisableCommandRule?) -> LintResult? {
superfluousDisableCommandRule: SuperfluousDisableCommandRule?,
compilerArguments: [String]) -> LintResult? {
if !(self is SourceKitFreeRule) && file.sourcekitdFailed {
return nil
}
@ -57,10 +58,10 @@ private extension Rule {
let ruleTime: (String, Double)?
if benchmark {
let start = Date()
violations = validate(file: file)
violations = validate(file: file, compilerArguments: compilerArguments)
ruleTime = (ruleID, -start.timeIntervalSinceNow)
} else {
violations = validate(file: file)
violations = validate(file: file, compilerArguments: compilerArguments)
ruleTime = nil
}
@ -104,6 +105,7 @@ public struct Linter {
private let rules: [Rule]
private let cache: LinterCache?
private let configuration: Configuration
private let compilerArguments: [String]
public var styleViolations: [StyleViolation] {
return getStyleViolations().0
@ -128,7 +130,8 @@ public struct Linter {
}) as? SuperfluousDisableCommandRule
let validationResults = rules.parallelFlatMap {
$0.lint(file: self.file, regions: regions, benchmark: benchmark,
superfluousDisableCommandRule: superfluousDisableCommandRule)
superfluousDisableCommandRule: superfluousDisableCommandRule,
compilerArguments: self.compilerArguments)
}
let violations = validationResults.flatMap { $0.violations }
let ruleTimes = validationResults.compactMap { $0.ruleTime }
@ -170,11 +173,13 @@ public struct Linter {
return (cachedViolations, ruleTimes)
}
public init(file: File, configuration: Configuration = Configuration()!, cache: LinterCache? = nil) {
public init(file: File, configuration: Configuration = Configuration()!, cache: LinterCache? = nil,
compilerArguments: [String] = []) {
self.file = file
self.cache = cache
self.configuration = configuration
rules = configuration.rules
self.cache = cache
self.compilerArguments = compilerArguments
self.rules = configuration.rules
}
public func correct() -> [Correction] {
@ -184,7 +189,7 @@ public struct Linter {
var corrections = [Correction]()
for rule in rules.compactMap({ $0 as? CorrectableRule }) {
let newCorrections = rule.correct(file: file)
let newCorrections = rule.correct(file: file, compilerArguments: compilerArguments)
corrections += newCorrections
if !newCorrections.isEmpty {
file.invalidateCache()

View File

@ -36,6 +36,7 @@ public let masterRuleList = RuleList(rules: [
ExplicitACLRule.self,
ExplicitEnumRawValueRule.self,
ExplicitInitRule.self,
ExplicitSelfRule.self,
ExplicitTopLevelACLRule.self,
ExplicitTypeInterfaceRule.self,
ExtensionAccessModifierRule.self,

View File

@ -8,6 +8,7 @@ public struct RuleDescription: Equatable {
public let corrections: [String: String]
public let deprecatedAliases: Set<String>
public let minSwiftVersion: SwiftVersion
public let requiresFileOnDisk: Bool
public var consoleDescription: String { return "\(name) (\(identifier)): \(description)" }
@ -19,7 +20,8 @@ public struct RuleDescription: Equatable {
minSwiftVersion: SwiftVersion = .three,
nonTriggeringExamples: [String] = [], triggeringExamples: [String] = [],
corrections: [String: String] = [:],
deprecatedAliases: Set<String> = []) {
deprecatedAliases: Set<String> = [],
requiresFileOnDisk: Bool = false) {
self.identifier = identifier
self.name = name
self.description = description
@ -29,6 +31,7 @@ public struct RuleDescription: Equatable {
self.corrections = corrections
self.deprecatedAliases = deprecatedAliases
self.minSwiftVersion = minSwiftVersion
self.requiresFileOnDisk = requiresFileOnDisk
}
}

View File

@ -7,11 +7,16 @@ public protocol Rule {
init() // Rules need to be able to be initialized with default values
init(configuration: Any) throws
func validate(file: File, compilerArguments: [String]) -> [StyleViolation]
func validate(file: File) -> [StyleViolation]
func isEqualTo(_ rule: Rule) -> Bool
}
extension Rule {
public func validate(file: File, compilerArguments: [String]) -> [StyleViolation] {
return validate(file: file)
}
public func isEqualTo(_ rule: Rule) -> Bool {
return type(of: self).description == type(of: rule).description
}
@ -32,11 +37,32 @@ public protocol ConfigurationProviderRule: Rule {
}
public protocol CorrectableRule: Rule {
func correct(file: File, compilerArguments: [String]) -> [Correction]
func correct(file: File) -> [Correction]
}
public extension CorrectableRule {
func correct(file: File, compilerArguments: [String]) -> [Correction] {
return correct(file: file)
}
}
public protocol SourceKitFreeRule: Rule {}
public protocol CompilerArgumentRule: Rule {}
public extension CompilerArgumentRule {
func validate(file: File) -> [StyleViolation] {
return validate(file: file, compilerArguments: [])
}
}
public extension CompilerArgumentRule where Self: CorrectableRule {
func correct(file: File) -> [Correction] {
return correct(file: file, compilerArguments: [])
}
}
// MARK: - ConfigurationProviderRule conformance to Configurable
public extension ConfigurationProviderRule {

View File

@ -0,0 +1,199 @@
import Foundation
import SourceKittenFramework
public struct ExplicitSelfRule: CorrectableRule, ConfigurationProviderRule, CompilerArgumentRule, OptInRule,
AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "explicit_self",
name: "Explicit Self",
description: "Instance variables and functions should be explicitly accessed with 'self.'.",
kind: .style,
nonTriggeringExamples: [
"""
struct A {
func f1() {}
func f2() {
self.f1()
}
}
""",
"""
struct A {
let p1: Int
func f1() {
_ = self.p1
}
}
"""
],
triggeringExamples: [
"""
struct A {
func f1() {}
func f2() {
f1()
}
}
""",
"""
struct A {
let p1: Int
func f1() {
_ = p1
}
}
"""
],
corrections: [
"""
struct A {
func f1() {}
func f2() {
f1()
}
}
""":
"""
struct A {
func f1() {}
func f2() {
self.f1()
}
}
""",
"""
struct A {
let p1: Int
func f1() {
_ = p1
}
}
""":
"""
struct A {
let p1: Int
func f1() {
_ = self.p1
}
}
"""
],
requiresFileOnDisk: true
)
public func validate(file: File, compilerArguments: [String]) -> [StyleViolation] {
return violationRanges(in: file, compilerArguments: compilerArguments).map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
public func correct(file: File, compilerArguments: [String]) -> [Correction] {
let violations = violationRanges(in: file, compilerArguments: compilerArguments)
let matches = file.ruleEnabled(violatingRanges: violations, for: self)
if matches.isEmpty { return [] }
var contents = file.contents.bridge()
let description = type(of: self).description
var corrections = [Correction]()
for range in matches.reversed() {
contents = contents.replacingCharacters(in: range, with: "self.").bridge()
let location = Location(file: file, characterOffset: range.location)
corrections.append(Correction(ruleDescription: description, location: location))
}
file.write(contents.bridge())
return corrections
}
private func violationRanges(in file: File, compilerArguments: [String]) -> [NSRange] {
guard !compilerArguments.isEmpty else {
return []
}
let allCursorInfo: [[String: SourceKitRepresentable]]
do {
let byteOffsets = try binaryOffsets(file: file, compilerArguments: compilerArguments)
allCursorInfo = try file.allCursorInfo(compilerArguments: compilerArguments,
atByteOffsets: byteOffsets)
} catch {
queuedPrintError(String(describing: error))
return []
}
let cursorsMissingExplicitSelf = allCursorInfo.filter { cursorInfo in
guard let kindString = cursorInfo["key.kind"] as? String else { return false }
return kindsToFind.contains(kindString)
}
guard !cursorsMissingExplicitSelf.isEmpty else {
return []
}
let contents = file.contents.bridge()
return cursorsMissingExplicitSelf.compactMap { cursorInfo in
guard let byteOffset = cursorInfo["swiftlint.offset"] as? Int64 else {
queuedPrintError("couldn't convert offsets")
return nil
}
return contents.byteRangeToNSRange(start: Int(byteOffset), length: 0)
}
}
}
private let kindsToFind = Set([
"source.lang.swift.ref.function.method.instance",
"source.lang.swift.ref.var.instance"
])
private extension File {
func allCursorInfo(compilerArguments: [String], atByteOffsets byteOffsets: [Int]) throws
-> [[String: SourceKitRepresentable]] {
return try byteOffsets.compactMap { offset in
if contents.bridge().substringWithByteRange(start: offset - 1, length: 1)! == "." { return nil }
var cursorInfo = try Request.cursorInfo(file: self.path!, offset: Int64(offset),
arguments: compilerArguments).send()
cursorInfo["swiftlint.offset"] = Int64(offset)
return cursorInfo
}
}
}
private extension NSString {
func byteOffset(forLine line: Int, column: Int) -> Int {
var byteOffset = 0
for line in lines()[..<(line - 1)] {
byteOffset += line.byteRange.length
}
return byteOffset + column - 1
}
func recursiveByteOffsets(_ dict: [String: Any]) -> [Int] {
let cur: [Int]
if let line = dict["key.line"] as? Int64,
let column = dict["key.column"] as? Int64,
let kindString = dict["key.kind"] as? String,
kindsToFind.contains(kindString) {
cur = [byteOffset(forLine: Int(line), column: Int(column))]
} else {
cur = []
}
if let entities = dict["key.entities"] as? [[String: Any]] {
return entities.flatMap(recursiveByteOffsets) + cur
}
return cur
}
}
private func binaryOffsets(file: File, compilerArguments: [String]) throws -> [Int] {
let absoluteFile = file.path!.bridge().absolutePathRepresentation()
let index = try Request.index(file: absoluteFile, arguments: compilerArguments).send()
let binaryOffsets = file.contents.bridge().recursiveByteOffsets(index)
return binaryOffsets.sorted()
}

View File

@ -7,47 +7,19 @@ struct AutoCorrectCommand: CommandProtocol {
let function = "Automatically correct warnings and errors"
func run(_ options: AutoCorrectOptions) -> Result<(), CommandantError<()>> {
let configuration = Configuration(options: options)
let cache = options.ignoreCache ? nil : LinterCache(configuration: configuration)
let indentWidth: Int
var useTabs: Bool
switch configuration.indentation {
case .tabs:
indentWidth = 4
useTabs = true
case .spaces(let count):
indentWidth = count
useTabs = false
}
if options.useTabs {
queuedPrintError("'use-tabs' is deprecated and will be completely removed" +
" in a future release. 'indentation' can now be defined in a configuration file.")
useTabs = options.useTabs
}
return configuration.visitLintableFiles(paths: options.paths, action: "Correcting",
quiet: options.quiet,
useScriptInputFiles: options.useScriptInputFiles,
forceExclude: options.forceExclude,
cache: cache, parallel: true) { linter in
let corrections = linter.correct()
if !corrections.isEmpty && !options.quiet {
let correctionLogs = corrections.map({ $0.consoleDescription })
queuedPrint(correctionLogs.joined(separator: "\n"))
}
if options.format {
linter.format(useTabs: useTabs, indentWidth: indentWidth)
}
}.flatMap { files in
if !options.quiet {
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
switch options.visitor {
case let .success(visitor):
return Configuration(options: options).visitLintableFiles(with: visitor).flatMap { files in
if !options.quiet {
let pluralSuffix = { (collection: [Any]) -> String in
return collection.count != 1 ? "s" : ""
}
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
}
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
return .success(())
}
return .success(())
case let .failure(error):
return .failure(error)
}
}
}
@ -62,22 +34,23 @@ struct AutoCorrectOptions: OptionsProtocol {
let cachePath: String
let ignoreCache: Bool
let useTabs: Bool
let compilerLogPath: String
// swiftlint:disable line_length
static func create(_ path: String) -> (_ configurationFile: String) -> (_ useScriptInputFiles: Bool) -> (_ quiet: Bool) -> (_ forceExclude: Bool) -> (_ format: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ useTabs: Bool) -> (_ paths: [String]) -> AutoCorrectOptions {
return { configurationFile in { useScriptInputFiles in { quiet in { forceExclude in { format in { cachePath in { ignoreCache in { useTabs in { paths in
static func create(_ path: String) -> (_ configurationFile: String) -> (_ useScriptInputFiles: Bool) -> (_ quiet: Bool) -> (_ forceExclude: Bool) -> (_ format: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ useTabs: Bool) -> (_ compilerLogPath: String) -> (_ paths: [String]) -> AutoCorrectOptions {
return { configurationFile in { useScriptInputFiles in { quiet in { forceExclude in { format in { cachePath in { ignoreCache in { useTabs in { compilerLogPath in { paths in
let allPaths: [String]
if !path.isEmpty {
allPaths = [path]
} else {
allPaths = paths
}
return self.init(paths: allPaths, configurationFile: configurationFile, useScriptInputFiles: useScriptInputFiles, quiet: quiet, forceExclude: forceExclude, format: format, cachePath: cachePath, ignoreCache: ignoreCache, useTabs: useTabs)
}}}}}}}}}
return self.init(paths: allPaths, configurationFile: configurationFile, useScriptInputFiles: useScriptInputFiles, quiet: quiet, forceExclude: forceExclude, format: format, cachePath: cachePath, ignoreCache: ignoreCache, useTabs: useTabs, compilerLogPath: compilerLogPath)
// swiftlint:enable line_length
}}}}}}}}}}
}
static func evaluate(_ mode: CommandMode) -> Result<AutoCorrectOptions, CommandantError<CommandantError<()>>> {
// swiftlint:enable line_length
return create
<*> mode <| pathOption(action: "correct")
<*> mode <| configOption
@ -96,7 +69,54 @@ struct AutoCorrectOptions: OptionsProtocol {
<*> mode <| Option(key: "use-tabs",
defaultValue: false,
usage: "should use tabs over spaces when reformatting. Deprecated.")
<*> mode <| Option(key: "compiler-log-path", defaultValue: "",
usage: """
the path of the full xcodebuild log to use when correcting CompilerArgumentRules
""")
// This should go last to avoid eating other args
<*> mode <| pathsArgument(action: "correct")
}
fileprivate var visitor: Result<LintableFilesVisitor, CommandantError<()>> {
let configuration = Configuration(options: self)
let cache = ignoreCache ? nil : LinterCache(configuration: configuration)
let indentWidth: Int
var useTabs: Bool
switch configuration.indentation {
case .tabs:
indentWidth = 4
useTabs = true
case .spaces(let count):
indentWidth = count
useTabs = false
}
if useTabs {
queuedPrintError("'use-tabs' is deprecated and will be completely removed" +
" in a future release. 'indentation' can now be defined in a configuration file.")
useTabs = self.useTabs
}
let visitor = LintableFilesVisitor(paths: paths, action: "Correcting", useSTDIN: false, quiet: quiet,
useScriptInputFiles: useScriptInputFiles, forceExclude: forceExclude,
cache: cache, parallel: true, compilerLogPath: compilerLogPath) { linter in
let corrections = linter.correct()
if !corrections.isEmpty && !self.quiet {
let correctionLogs = corrections.map({ $0.consoleDescription })
queuedPrint(correctionLogs.joined(separator: "\n"))
}
if self.format {
linter.format(useTabs: useTabs, indentWidth: indentWidth)
}
}
if let visitor = visitor {
return .success(visitor)
}
return .failure(
.usageError(description: "Could not read compiler log at path: '\(compilerLogPath)'")
)
}
}

View File

@ -130,18 +130,19 @@ struct LintOptions: OptionsProtocol {
let cachePath: String
let ignoreCache: Bool
let enableAllRules: Bool
let compilerLogPath: String
// swiftlint:disable line_length
static func create(_ path: String) -> (_ useSTDIN: Bool) -> (_ configurationFile: String) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ enableAllRules: Bool) -> (_ paths: [String]) -> LintOptions {
return { useSTDIN in { configurationFile in { strict in { lenient in { forceExclude in { useScriptInputFiles in { benchmark in { reporter in { quiet in { cachePath in { ignoreCache in { enableAllRules in { paths in
static func create(_ path: String) -> (_ useSTDIN: Bool) -> (_ configurationFile: String) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ enableAllRules: Bool) -> (_ compilerLogPath: String) -> (_ paths: [String]) -> LintOptions {
return { useSTDIN in { configurationFile in { strict in { lenient in { forceExclude in { useScriptInputFiles in { benchmark in { reporter in { quiet in { cachePath in { ignoreCache in { enableAllRules in { compilerLogPath in { paths in
let allPaths: [String]
if !path.isEmpty {
allPaths = [path]
} else {
allPaths = paths
}
return self.init(paths: allPaths, useSTDIN: useSTDIN, configurationFile: configurationFile, strict: strict, lenient: lenient, forceExclude: forceExclude, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, cachePath: cachePath, ignoreCache: ignoreCache, enableAllRules: enableAllRules)
}}}}}}}}}}}}}
return self.init(paths: allPaths, useSTDIN: useSTDIN, configurationFile: configurationFile, strict: strict, lenient: lenient, forceExclude: forceExclude, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, cachePath: cachePath, ignoreCache: ignoreCache, enableAllRules: enableAllRules, compilerLogPath: compilerLogPath)
}}}}}}}}}}}}}}
}
static func evaluate(_ mode: CommandMode) -> Result<LintOptions, CommandantError<CommandantError<()>>> {
@ -170,6 +171,8 @@ struct LintOptions: OptionsProtocol {
usage: "ignore cache when linting")
<*> mode <| Option(key: "enable-all-rules", defaultValue: false,
usage: "run all rules, even opt-in and disabled ones, ignoring `whitelist_rules`")
<*> mode <| Option(key: "compiler-log-path", defaultValue: "",
usage: "the path of the full xcodebuild log to use when linting CompilerArgumentRules")
// This should go last to avoid eating other args
<*> mode <| pathsArgument(action: "lint")
}

View File

@ -48,84 +48,92 @@ private func autoreleasepool(block: () -> Void) { block() }
extension Configuration {
func visitLintableFiles(paths: [String], action: String, useSTDIN: Bool = false,
quiet: Bool = false, useScriptInputFiles: Bool, forceExclude: Bool,
cache: LinterCache? = nil, parallel: Bool = false,
visitorBlock: @escaping (Linter) -> Void) -> Result<[File], CommandantError<()>> {
return getFiles(paths: paths, action: action, useSTDIN: useSTDIN, quiet: quiet, forceExclude: forceExclude,
useScriptInputFiles: useScriptInputFiles)
.flatMap { files -> Result<[Configuration: [File]], CommandantError<()>> in
if files.isEmpty {
let errorMessage = "No lintable files found at paths: '\(paths.joined(separator: ", "))'"
return .failure(.usageError(description: errorMessage))
}
return .success(Dictionary(grouping: files, by: configuration(for:)))
}.flatMap { filesPerConfiguration in
let queue = DispatchQueue(label: "io.realm.swiftlint.indexIncrementer")
var index = 0
let fileCount = filesPerConfiguration.reduce(0) { $0 + $1.value.count }
let visit = { (file: File, config: Configuration) -> Void in
if !quiet, let path = file.path {
let increment = {
index += 1
let filename = path.bridge().lastPathComponent
queuedPrintError("\(action) '\(filename)' (\(index)/\(fileCount))")
}
if parallel {
queue.sync(execute: increment)
} else {
increment()
}
}
autoreleasepool {
visitorBlock(Linter(file: file, configuration: config, cache: cache))
}
}
var filesAndConfigurations = [(File, Configuration)]()
filesAndConfigurations.reserveCapacity(fileCount)
for (config, files) in filesPerConfiguration {
let newConfig: Configuration
if cache != nil {
newConfig = config.withPrecomputedCacheDescription()
} else {
newConfig = config
}
filesAndConfigurations += files.map { ($0, newConfig) }
}
if parallel {
DispatchQueue.concurrentPerform(iterations: fileCount) { index in
let (file, config) = filesAndConfigurations[index]
visit(file, config)
}
} else {
filesAndConfigurations.forEach(visit)
}
return .success(filesAndConfigurations.compactMap({ $0.0 }))
}
func visitLintableFiles(with visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
return getFiles(with: visitor)
.flatMap { groupFiles($0, visitor: visitor) }
.flatMap { visit(filesPerConfiguration: $0, visitor: visitor) }
}
// swiftlint:disable function_parameter_count
fileprivate func getFiles(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, forceExclude: Bool,
useScriptInputFiles: Bool) -> Result<[File], CommandantError<()>> {
if useSTDIN {
private func groupFiles(_ files: [File],
visitor: LintableFilesVisitor) -> Result<[Configuration: [File]], CommandantError<()>> {
if files.isEmpty {
let errorMessage = "No lintable files found at paths: '\(visitor.paths.joined(separator: ", "))'"
return .failure(.usageError(description: errorMessage))
}
return .success(Dictionary(grouping: files, by: configuration(for:)))
}
private func visit(filesPerConfiguration: [Configuration: [File]],
visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
let queue = DispatchQueue(label: "io.realm.swiftlint.indexIncrementer")
var index = 0
let fileCount = filesPerConfiguration.reduce(0) { $0 + $1.value.count }
let visit = { (file: File, config: Configuration) -> Void in
if !visitor.quiet, let path = file.path {
let increment = {
index += 1
let filename = path.bridge().lastPathComponent
queuedPrintError("\(visitor.action) '\(filename)' (\(index)/\(fileCount))")
}
if visitor.parallel {
queue.sync(execute: increment)
} else {
increment()
}
}
autoreleasepool {
let compilerArguments = file.compilerArguments(compilerLogs: visitor.compilerLogContents)
let linter = Linter(file: file, configuration: config, cache: visitor.cache,
compilerArguments: compilerArguments)
visitor.block(linter)
}
}
var filesAndConfigurations = [(File, Configuration)]()
filesAndConfigurations.reserveCapacity(fileCount)
for (config, files) in filesPerConfiguration {
let newConfig: Configuration
if visitor.cache != nil {
newConfig = config.withPrecomputedCacheDescription()
} else {
newConfig = config
}
filesAndConfigurations += files.map { ($0, newConfig) }
}
if visitor.parallel {
DispatchQueue.concurrentPerform(iterations: fileCount) { index in
let (file, config) = filesAndConfigurations[index]
visit(file, config)
}
} else {
filesAndConfigurations.forEach(visit)
}
return .success(filesAndConfigurations.compactMap({ $0.0 }))
}
fileprivate func getFiles(with visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
if visitor.useSTDIN {
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
if let stdinString = String(data: stdinData, encoding: .utf8) {
return .success([File(contents: stdinString)])
}
return .failure(.usageError(description: "stdin isn't a UTF8-encoded string"))
} else if useScriptInputFiles {
} else if visitor.useScriptInputFiles {
return scriptInputFiles()
}
if !quiet {
let filesInfo = paths.isEmpty ? "in current working directory" : "at paths \(paths.joined(separator: ", "))"
let message = "\(action) Swift files \(filesInfo)"
queuedPrintError(message)
if !visitor.quiet {
let filesInfo: String
if visitor.paths.isEmpty {
filesInfo = "in current working directory"
} else {
filesInfo = "at paths \(visitor.paths.joined(separator: ", "))"
}
queuedPrintError("\(visitor.action) Swift files \(filesInfo)")
}
return .success(paths.flatMap {
self.lintableFiles(inPath: $0, forceExclude: forceExclude)
return .success(visitor.paths.flatMap {
self.lintableFiles(inPath: $0, forceExclude: visitor.forceExclude)
})
}
// swiftlint:enable function_parameter_count
private static func rootPath(from paths: [String]) -> String? {
// We don't know the root when more than one path is passed (i.e. not useful if the root of 2 paths is ~)
@ -146,10 +154,17 @@ extension Configuration {
func visitLintableFiles(options: LintOptions, cache: LinterCache? = nil,
visitorBlock: @escaping (Linter) -> Void) -> Result<[File], CommandantError<()>> {
return visitLintableFiles(paths: options.paths, action: "Linting", useSTDIN: options.useSTDIN,
quiet: options.quiet, useScriptInputFiles: options.useScriptInputFiles,
forceExclude: options.forceExclude, cache: cache, parallel: true,
visitorBlock: visitorBlock)
let visitor = LintableFilesVisitor(paths: options.paths, action: "Linting", useSTDIN: options.useSTDIN,
quiet: options.quiet, useScriptInputFiles: options.useScriptInputFiles,
forceExclude: options.forceExclude, cache: cache, parallel: true,
compilerLogPath: options.compilerLogPath, block: visitorBlock)
if let visitor = visitor {
return visitLintableFiles(with: visitor)
}
return .failure(
.usageError(description: "Could not read compiler log at path: '\(options.compilerLogPath)'")
)
}
// MARK: AutoCorrect Command

View File

@ -0,0 +1,136 @@
import Foundation
import SourceKittenFramework
extension File {
/// Extracts compiler arguments for this file from the specified compiler logs.
///
/// - parameter compilerLogs: xcodebuild log output from a clean & successful build (not incremental).
///
/// - returns: The compiler arguments for this file, or an empty array if none were found.
func compilerArguments(compilerLogs: String) -> [String] {
return path.flatMap { path in
return compileCommand(compilerLogs: compilerLogs, sourceFile: path)
} ?? []
}
}
// MARK: - Private
#if os(Linux)
private extension Scanner {
func scanString(string: String) -> String? {
return scanString(string)
}
}
#else
private extension Scanner {
func scanUpToString(_ string: String) -> String? {
var result: NSString?
let success = scanUpTo(string, into: &result)
if success {
return result?.bridge()
}
return nil
}
func scanString(string: String) -> String? {
var result: NSString?
let success = scanString(string, into: &result)
if success {
return result?.bridge()
}
return nil
}
}
#endif
private func compileCommand(compilerLogs: String, sourceFile: String) -> [String]? {
let escapedSourceFile = sourceFile.replacingOccurrences(of: " ", with: "\\ ")
guard compilerLogs.contains(escapedSourceFile) else {
return nil
}
var compileCommand: [String]?
compilerLogs.enumerateLines { line, stop in
if line.contains(escapedSourceFile),
let swiftcIndex = line.range(of: "swiftc ")?.upperBound,
line.contains(" -module-name ") {
compileCommand = parseCLIArguments(String(line[swiftcIndex...]))
stop = true
}
}
return compileCommand
}
private func parseCLIArguments(_ string: String) -> [String] {
let escapedSpacePlaceholder = "\u{0}"
let scanner = Scanner(string: string)
var str = ""
var didStart = false
while let result = scanner.scanUpToString("\"") {
if didStart {
str += result.replacingOccurrences(of: " ", with: escapedSpacePlaceholder)
str += " "
} else {
str += result
}
_ = scanner.scanString(string: "\"")
didStart = !didStart
}
return filter(arguments:
str.trimmingCharacters(in: .whitespaces)
.replacingOccurrences(of: "\\ ", with: escapedSpacePlaceholder)
.components(separatedBy: " ")
.map { $0.replacingOccurrences(of: escapedSpacePlaceholder, with: " ") }
)
}
/**
Partially filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: A tuple of partially filtered compiler arguments in `.0`, and whether or not there are
more flags to remove in `.1`.
*/
private func partiallyFilter(arguments args: [String]) -> ([String], Bool) {
guard let indexOfFlagToRemove = args.index(of: "-output-file-map") else {
return (args, false)
}
var args = args
args.remove(at: args.index(after: indexOfFlagToRemove))
args.remove(at: indexOfFlagToRemove)
return (args, true)
}
/**
Filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: Filtered compiler arguments.
*/
private func filter(arguments args: [String]) -> [String] {
var args = args
args.append(contentsOf: ["-D", "DEBUG"])
var shouldContinueToFilterArguments = true
while shouldContinueToFilterArguments {
(args, shouldContinueToFilterArguments) = partiallyFilter(arguments: args)
}
return args.filter {
![
"-parseable-output",
"-incremental",
"-serialize-diagnostics",
"-emit-dependencies"
].contains($0)
}.map {
if $0 == "-O" {
return "-Onone"
} else if $0 == "-DNDEBUG=1" {
return "-DDEBUG=1"
}
return $0
}
}

View File

@ -0,0 +1,43 @@
import Foundation
import SwiftLintFramework
struct LintableFilesVisitor {
let paths: [String]
let action: String
let useSTDIN: Bool
let quiet: Bool
let useScriptInputFiles: Bool
let forceExclude: Bool
let cache: LinterCache?
let parallel: Bool
let compilerLogContents: String
let block: (Linter) -> Void
init?(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, useScriptInputFiles: Bool, forceExclude: Bool,
cache: LinterCache?, parallel: Bool, compilerLogPath: String, block: @escaping (Linter) -> Void) {
self.paths = paths
self.action = action
self.useSTDIN = useSTDIN
self.quiet = quiet
self.useScriptInputFiles = useScriptInputFiles
self.forceExclude = forceExclude
self.cache = cache
self.parallel = parallel
self.compilerLogContents = LintableFilesVisitor.compilerLogContents(logPath: compilerLogPath)
self.block = block
}
private static func compilerLogContents(logPath: String) -> String {
if logPath.isEmpty {
return ""
}
if let data = FileManager.default.contents(atPath: logPath),
let logContents = String(data: data, encoding: .utf8) {
return logContents
}
print("couldn't read log file at path '\(logPath)'")
return ""
}
}

View File

@ -142,7 +142,10 @@
8B01E50220A4349100C9233E /* FunctionParameterCountRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B01E4FF20A4340A00C9233E /* FunctionParameterCountRuleTests.swift */; };
8F2CC1CB20A6A070006ED34F /* FileNameConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CA20A6A070006ED34F /* FileNameConfiguration.swift */; };
8F2CC1CD20A6A189006ED34F /* FileNameRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CC20A6A189006ED34F /* FileNameRuleTests.swift */; };
8F6AA75B211905B8009BA28A /* LintableFilesVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6AA75A211905B8009BA28A /* LintableFilesVisitor.swift */; };
8F6AA75D21190830009BA28A /* File+CompilerArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6AA75C21190830009BA28A /* File+CompilerArguments.swift */; };
8F8050821FFE0CBB006F5B93 /* Configuration+IndentationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F8050811FFE0CBB006F5B93 /* Configuration+IndentationStyle.swift */; };
8FC8523B2117BDDE0015269B /* ExplicitSelfRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC8523A2117BDDE0015269B /* ExplicitSelfRule.swift */; };
8FC9F5111F4B8E48006826C1 /* IsDisjointRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC9F5101F4B8E48006826C1 /* IsDisjointRule.swift */; };
8FD216CC205584AF008ED13F /* CharacterSet+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD216CB205584AF008ED13F /* CharacterSet+SwiftLint.swift */; };
92CCB2D71E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */; };
@ -543,7 +546,10 @@
8B01E4FF20A4340A00C9233E /* FunctionParameterCountRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionParameterCountRuleTests.swift; sourceTree = "<group>"; };
8F2CC1CA20A6A070006ED34F /* FileNameConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileNameConfiguration.swift; sourceTree = "<group>"; };
8F2CC1CC20A6A189006ED34F /* FileNameRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileNameRuleTests.swift; sourceTree = "<group>"; };
8F6AA75A211905B8009BA28A /* LintableFilesVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LintableFilesVisitor.swift; sourceTree = "<group>"; };
8F6AA75C21190830009BA28A /* File+CompilerArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+CompilerArguments.swift"; sourceTree = "<group>"; };
8F8050811FFE0CBB006F5B93 /* Configuration+IndentationStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+IndentationStyle.swift"; sourceTree = "<group>"; };
8FC8523A2117BDDE0015269B /* ExplicitSelfRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplicitSelfRule.swift; sourceTree = "<group>"; };
8FC9F5101F4B8E48006826C1 /* IsDisjointRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsDisjointRule.swift; sourceTree = "<group>"; };
8FD216CB205584AF008ED13F /* CharacterSet+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CharacterSet+SwiftLint.swift"; sourceTree = "<group>"; };
92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedOptionalBindingRule.swift; sourceTree = "<group>"; };
@ -989,6 +995,7 @@
D4470D581EB6B4D1008A1B2E /* EmptyEnumArgumentsRule.swift */,
D47079AC1DFE2FA700027086 /* EmptyParametersRule.swift */,
D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */,
8FC8523A2117BDDE0015269B /* ExplicitSelfRule.swift */,
D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */,
E88DEA931B099C0900A66CB0 /* IdentifierNameRule.swift */,
D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */,
@ -1310,6 +1317,7 @@
children = (
E802ECFF1C56A56000A35AE1 /* Benchmark.swift */,
E81FB3E31C6D507B00DC988F /* CommonOptions.swift */,
8F6AA75A211905B8009BA28A /* LintableFilesVisitor.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1428,6 +1436,7 @@
isa = PBXGroup;
children = (
E8B067801C13E49600E9E13F /* Configuration+CommandLine.swift */,
8F6AA75C21190830009BA28A /* File+CompilerArguments.swift */,
E86E2B2D1E17443B001E823C /* Reporter+CommandLine.swift */,
6C032EF02027F79F00CD7E8D /* shim.swift */,
);
@ -1933,6 +1942,7 @@
B89F3BCF1FD5EE1400931E59 /* RequiredEnumCaseRuleConfiguration.swift in Sources */,
D48B51211F4F5DEF0068AB98 /* RuleList+Documentation.swift in Sources */,
8FC9F5111F4B8E48006826C1 /* IsDisjointRule.swift in Sources */,
8FC8523B2117BDDE0015269B /* ExplicitSelfRule.swift in Sources */,
4DCB8E7F1CBE494E0070FCF0 /* RegexHelpers.swift in Sources */,
E86396C21BADAAE5002C9E88 /* Reporter.swift in Sources */,
A1A6F3F21EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift in Sources */,
@ -2021,6 +2031,7 @@
buildActionMask = 2147483647;
files = (
E86E2B2E1E17443B001E823C /* Reporter+CommandLine.swift in Sources */,
8F6AA75D21190830009BA28A /* File+CompilerArguments.swift in Sources */,
E8B067811C13E49600E9E13F /* Configuration+CommandLine.swift in Sources */,
E802ED001C56A56000A35AE1 /* Benchmark.swift in Sources */,
E83A0B351A5D382B0041A60A /* VersionCommand.swift in Sources */,
@ -2031,6 +2042,7 @@
6C032EF12027F79F00CD7E8D /* shim.swift in Sources */,
D4DA1DFC1E19CD300037413D /* GenerateDocsCommand.swift in Sources */,
E84E07471C13F95300F11122 /* AutoCorrectCommand.swift in Sources */,
8F6AA75B211905B8009BA28A /* LintableFilesVisitor.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -332,6 +332,12 @@ extension ExplicitInitRuleTests {
]
}
extension ExplicitSelfRuleTests {
static var allTests: [(String, (ExplicitSelfRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
]
}
extension ExplicitTopLevelACLRuleTests {
static var allTests: [(String, (ExplicitTopLevelACLRuleTests) -> () throws -> Void)] = [
("testWithDefaultConfiguration", testWithDefaultConfiguration)
@ -1274,6 +1280,7 @@ XCTMain([
testCase(ExplicitACLRuleTests.allTests),
testCase(ExplicitEnumRawValueRuleTests.allTests),
testCase(ExplicitInitRuleTests.allTests),
testCase(ExplicitSelfRuleTests.allTests),
testCase(ExplicitTopLevelACLRuleTests.allTests),
testCase(ExplicitTypeInterfaceConfigurationTests.allTests),
testCase(ExplicitTypeInterfaceRuleTests.allTests),

View File

@ -174,6 +174,12 @@ class ExplicitInitRuleTests: XCTestCase {
}
}
class ExplicitSelfRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(ExplicitSelfRule.description)
}
}
class ExplicitTopLevelACLRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(ExplicitTopLevelACLRule.description)

View File

@ -13,11 +13,41 @@ extension String {
let allRuleIdentifiers = Array(masterRuleList.list.keys)
func violations(_ string: String, config: Configuration = Configuration()!) -> [StyleViolation] {
func violations(_ string: String, config: Configuration = Configuration()!,
requiresFileOnDisk: Bool = false) -> [StyleViolation] {
File.clearCaches()
let stringStrippingMarkers = string.replacingOccurrences(of: violationMarker, with: "")
let file = File(contents: stringStrippingMarkers)
return Linter(file: file, configuration: config).styleViolations
guard requiresFileOnDisk else {
let file = File(contents: stringStrippingMarkers)
let linter = Linter(file: file, configuration: config)
return linter.styleViolations
}
let file = temporaryFile(contents: stringStrippingMarkers)
let linter = linterWithCompilerArguments(file, config: config)
return linter.styleViolations.map { violation in
let locationWithoutFile = Location(file: nil,
line: violation.location.line,
character: violation.location.character)
return StyleViolation(ruleDescription: violation.ruleDescription, severity: violation.severity,
location: locationWithoutFile, reason: violation.reason)
}
}
private func linterWithCompilerArguments(_ file: File, config: Configuration) -> Linter {
return Linter(file: file, configuration: config, compilerArguments: ["-j4", file.path!])
}
private func temporaryFile(contents: String) -> File {
let url = temporaryFileURL()!
_ = try? contents.data(using: .utf8)!.write(to: url)
return File(path: url.path)!
}
private func temporaryFileURL() -> URL? {
return URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("swift")
}
private func cleanedContentsAndMarkerOffsets(from contents: String) -> (String, [Int]) {
@ -65,10 +95,9 @@ private func render(locations: [Location], in contents: String) -> String {
private extension Configuration {
func assertCorrection(_ before: String, expected: String) {
guard let path = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(NSUUID().uuidString + ".swift")?.path else {
XCTFail("couldn't generate temporary path for assertCorrection()")
return
guard let path = temporaryFileURL()?.path else {
XCTFail("couldn't generate temporary path for assertCorrection()")
return
}
let (cleanedBefore, markerOffsets) = cleanedContentsAndMarkerOffsets(from: before)
do {
@ -83,7 +112,7 @@ private extension Configuration {
}
// expectedLocations are needed to create before call `correct()`
let expectedLocations = markerOffsets.map { Location(file: file, characterOffset: $0) }
let corrections = Linter(file: file, configuration: self).correct().sorted {
let corrections = linterWithCompilerArguments(file, config: self).correct().sorted {
$0.location < $1.location
}
if expectedLocations.isEmpty {
@ -152,6 +181,10 @@ extension XCTestCase {
skipDisableCommandTests: Bool = false,
testMultiByteOffsets: Bool = true,
testShebang: Bool = true) {
guard SwiftVersion.current >= ruleDescription.minSwiftVersion else {
return
}
guard let config = makeConfig(ruleConfiguration,
ruleDescription.identifier,
skipDisableCommandTests: skipDisableCommandTests) else {
@ -159,40 +192,6 @@ extension XCTestCase {
return
}
guard SwiftVersion.current >= ruleDescription.minSwiftVersion else {
return
}
let triggers = ruleDescription.triggeringExamples
let nonTriggers = ruleDescription.nonTriggeringExamples
verifyExamples(triggers: triggers, nonTriggers: nonTriggers, configuration: config)
if testMultiByteOffsets {
verifyExamples(triggers: triggers.map(addEmoji),
nonTriggers: nonTriggers.map(addEmoji), configuration: config)
}
if testShebang {
verifyExamples(triggers: triggers.map(addShebang),
nonTriggers: nonTriggers.map(addShebang), configuration: config)
}
// Comment doesn't violate
if !skipCommentTests {
XCTAssertEqual(
triggers.flatMap({ violations("/*\n " + $0 + "\n */", config: config) }).count,
commentDoesntViolate ? 0 : triggers.count
)
}
// String doesn't violate
if !skipStringTests {
XCTAssertEqual(
triggers.flatMap({ violations($0.toStringLiteral(), config: config) }).count,
stringDoesntViolate ? 0 : triggers.count
)
}
let disableCommands: [String]
if skipDisableCommandTests {
disableCommands = []
@ -200,18 +199,75 @@ extension XCTestCase {
disableCommands = ruleDescription.allIdentifiers.map { "// swiftlint:disable \($0)\n" }
}
// "disable" commands doesn't violate
for command in disableCommands {
XCTAssert(triggers.flatMap({ violations(command + $0, config: config) }).isEmpty)
self.verifyLint(ruleDescription, config: config, commentDoesntViolate: commentDoesntViolate,
stringDoesntViolate: stringDoesntViolate, skipCommentTests: skipCommentTests,
skipStringTests: skipStringTests, disableCommands: disableCommands,
testMultiByteOffsets: testMultiByteOffsets, testShebang: testShebang)
self.verifyCorrections(ruleDescription, config: config, disableCommands: disableCommands,
testMultiByteOffsets: testMultiByteOffsets)
}
func verifyLint(_ ruleDescription: RuleDescription,
config: Configuration,
commentDoesntViolate: Bool = true,
stringDoesntViolate: Bool = true,
skipCommentTests: Bool = false,
skipStringTests: Bool = false,
disableCommands: [String] = [],
testMultiByteOffsets: Bool = true,
testShebang: Bool = true) {
func verify(triggers: [String], nonTriggers: [String]) {
verifyExamples(triggers: triggers, nonTriggers: nonTriggers, configuration: config,
requiresFileOnDisk: ruleDescription.requiresFileOnDisk)
}
let triggers = ruleDescription.triggeringExamples
let nonTriggers = ruleDescription.nonTriggeringExamples
verify(triggers: triggers, nonTriggers: nonTriggers)
if testMultiByteOffsets {
verify(triggers: triggers.map(addEmoji), nonTriggers: nonTriggers.map(addEmoji))
}
if testShebang {
verify(triggers: triggers.map(addShebang), nonTriggers: nonTriggers.map(addShebang))
}
func makeViolations(_ string: String) -> [StyleViolation] {
return violations(string, config: config, requiresFileOnDisk: ruleDescription.requiresFileOnDisk)
}
// Comment doesn't violate
if !skipCommentTests {
XCTAssertEqual(
triggers.flatMap({ makeViolations("/*\n " + $0 + "\n */") }).count,
commentDoesntViolate ? 0 : triggers.count
)
}
// String doesn't violate
if !skipStringTests {
XCTAssertEqual(
triggers.flatMap({ makeViolations($0.toStringLiteral()) }).count,
stringDoesntViolate ? 0 : triggers.count
)
}
// "disable" commands doesn't violate
for command in disableCommands {
XCTAssert(triggers.flatMap({ makeViolations(command + $0) }).isEmpty)
}
}
func verifyCorrections(_ ruleDescription: RuleDescription, config: Configuration,
disableCommands: [String], testMultiByteOffsets: Bool) {
// corrections
ruleDescription.corrections.forEach {
testCorrection($0, configuration: config, testMultiByteOffsets: testMultiByteOffsets)
}
// make sure strings that don't trigger aren't corrected
zip(nonTriggers, nonTriggers).forEach {
testCorrection($0, configuration: config, testMultiByteOffsets: testMultiByteOffsets)
ruleDescription.nonTriggeringExamples.forEach {
testCorrection(($0, $0), configuration: config, testMultiByteOffsets: testMultiByteOffsets)
}
// "disable" commands do not correct
@ -222,14 +278,14 @@ extension XCTestCase {
config.assertCorrection(expectedCleaned, expected: expectedCleaned)
}
}
}
private func verifyExamples(triggers: [String], nonTriggers: [String],
configuration config: Configuration) {
configuration config: Configuration, requiresFileOnDisk: Bool) {
// Non-triggering examples don't violate
for nonTrigger in nonTriggers {
let unexpectedViolations = violations(nonTrigger, config: config)
let unexpectedViolations = violations(nonTrigger, config: config,
requiresFileOnDisk: requiresFileOnDisk)
if unexpectedViolations.isEmpty { continue }
let nonTriggerWithViolations = render(violations: unexpectedViolations, in: nonTrigger)
XCTFail("nonTriggeringExample violated: \n\(nonTriggerWithViolations)")
@ -237,7 +293,8 @@ extension XCTestCase {
// Triggering examples violate
for trigger in triggers {
let triggerViolations = violations(trigger, config: config)
let triggerViolations = violations(trigger, config: config,
requiresFileOnDisk: requiresFileOnDisk)
// Triggering examples with violation markers violate at the marker's location
let (cleanTrigger, markerOffsets) = cleanedContentsAndMarkerOffsets(from: trigger)

View File

@ -84,6 +84,6 @@ class TrailingCommaRuleTests: XCTestCase {
private func trailingCommaViolations(_ string: String, ruleConfiguration: Any? = nil) -> [StyleViolation] {
let config = makeConfig(ruleConfiguration, TrailingCommaRule.description.identifier)!
return SwiftLintFrameworkTests.violations(string, config: config)
return violations(string, config: config)
}
}