Compare commits
10 Commits
main
...
compiled-m
Author | SHA1 | Date |
---|---|---|
![]() |
5257b61fe9 | |
![]() |
5d91e3a73c | |
![]() |
5b53a34055 | |
![]() |
1f0a28944a | |
![]() |
cfdf76e4dd | |
![]() |
3e6bea7964 | |
![]() |
142c5c3304 | |
![]() |
132a395c76 | |
![]() |
bd54b861ae | |
![]() |
aee843e1b4 |
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -15,6 +15,19 @@
|
||||||
[Ornithologist Coder](https://github.com/ornithocoder)
|
[Ornithologist Coder](https://github.com/ornithocoder)
|
||||||
[#52](https://github.com/realm/SwiftLint/issues/52)
|
[#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
|
#### Bug Fixes
|
||||||
|
|
||||||
* Fix `comma` rule false positives on object literals (for example, images).
|
* Fix `comma` rule false positives on object literals (for example, images).
|
||||||
|
|
58
Rules.md
58
Rules.md
|
@ -35,6 +35,7 @@
|
||||||
* [Explicit ACL](#explicit-acl)
|
* [Explicit ACL](#explicit-acl)
|
||||||
* [Explicit Enum Raw Value](#explicit-enum-raw-value)
|
* [Explicit Enum Raw Value](#explicit-enum-raw-value)
|
||||||
* [Explicit Init](#explicit-init)
|
* [Explicit Init](#explicit-init)
|
||||||
|
* [Explicit Self](#explicit-self)
|
||||||
* [Explicit Top Level ACL](#explicit-top-level-acl)
|
* [Explicit Top Level ACL](#explicit-top-level-acl)
|
||||||
* [Explicit Type Interface](#explicit-type-interface)
|
* [Explicit Type Interface](#explicit-type-interface)
|
||||||
* [Extension Access Modifier](#extension-access-modifier)
|
* [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
|
## Explicit Top Level ACL
|
||||||
|
|
||||||
Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version
|
Identifier | Enabled by default | Supports autocorrection | Kind | Minimum Swift Compiler Version
|
||||||
|
|
|
@ -46,7 +46,8 @@ private extension Rule {
|
||||||
}
|
}
|
||||||
|
|
||||||
func lint(file: File, regions: [Region], benchmark: Bool,
|
func lint(file: File, regions: [Region], benchmark: Bool,
|
||||||
superfluousDisableCommandRule: SuperfluousDisableCommandRule?) -> LintResult? {
|
superfluousDisableCommandRule: SuperfluousDisableCommandRule?,
|
||||||
|
compilerArguments: [String]) -> LintResult? {
|
||||||
if !(self is SourceKitFreeRule) && file.sourcekitdFailed {
|
if !(self is SourceKitFreeRule) && file.sourcekitdFailed {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -57,10 +58,10 @@ private extension Rule {
|
||||||
let ruleTime: (String, Double)?
|
let ruleTime: (String, Double)?
|
||||||
if benchmark {
|
if benchmark {
|
||||||
let start = Date()
|
let start = Date()
|
||||||
violations = validate(file: file)
|
violations = validate(file: file, compilerArguments: compilerArguments)
|
||||||
ruleTime = (ruleID, -start.timeIntervalSinceNow)
|
ruleTime = (ruleID, -start.timeIntervalSinceNow)
|
||||||
} else {
|
} else {
|
||||||
violations = validate(file: file)
|
violations = validate(file: file, compilerArguments: compilerArguments)
|
||||||
ruleTime = nil
|
ruleTime = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +105,7 @@ public struct Linter {
|
||||||
private let rules: [Rule]
|
private let rules: [Rule]
|
||||||
private let cache: LinterCache?
|
private let cache: LinterCache?
|
||||||
private let configuration: Configuration
|
private let configuration: Configuration
|
||||||
|
private let compilerArguments: [String]
|
||||||
|
|
||||||
public var styleViolations: [StyleViolation] {
|
public var styleViolations: [StyleViolation] {
|
||||||
return getStyleViolations().0
|
return getStyleViolations().0
|
||||||
|
@ -128,7 +130,8 @@ public struct Linter {
|
||||||
}) as? SuperfluousDisableCommandRule
|
}) as? SuperfluousDisableCommandRule
|
||||||
let validationResults = rules.parallelFlatMap {
|
let validationResults = rules.parallelFlatMap {
|
||||||
$0.lint(file: self.file, regions: regions, benchmark: benchmark,
|
$0.lint(file: self.file, regions: regions, benchmark: benchmark,
|
||||||
superfluousDisableCommandRule: superfluousDisableCommandRule)
|
superfluousDisableCommandRule: superfluousDisableCommandRule,
|
||||||
|
compilerArguments: self.compilerArguments)
|
||||||
}
|
}
|
||||||
let violations = validationResults.flatMap { $0.violations }
|
let violations = validationResults.flatMap { $0.violations }
|
||||||
let ruleTimes = validationResults.compactMap { $0.ruleTime }
|
let ruleTimes = validationResults.compactMap { $0.ruleTime }
|
||||||
|
@ -170,11 +173,13 @@ public struct Linter {
|
||||||
return (cachedViolations, ruleTimes)
|
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.file = file
|
||||||
self.cache = cache
|
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
rules = configuration.rules
|
self.cache = cache
|
||||||
|
self.compilerArguments = compilerArguments
|
||||||
|
self.rules = configuration.rules
|
||||||
}
|
}
|
||||||
|
|
||||||
public func correct() -> [Correction] {
|
public func correct() -> [Correction] {
|
||||||
|
@ -184,7 +189,7 @@ public struct Linter {
|
||||||
|
|
||||||
var corrections = [Correction]()
|
var corrections = [Correction]()
|
||||||
for rule in rules.compactMap({ $0 as? CorrectableRule }) {
|
for rule in rules.compactMap({ $0 as? CorrectableRule }) {
|
||||||
let newCorrections = rule.correct(file: file)
|
let newCorrections = rule.correct(file: file, compilerArguments: compilerArguments)
|
||||||
corrections += newCorrections
|
corrections += newCorrections
|
||||||
if !newCorrections.isEmpty {
|
if !newCorrections.isEmpty {
|
||||||
file.invalidateCache()
|
file.invalidateCache()
|
||||||
|
|
|
@ -36,6 +36,7 @@ public let masterRuleList = RuleList(rules: [
|
||||||
ExplicitACLRule.self,
|
ExplicitACLRule.self,
|
||||||
ExplicitEnumRawValueRule.self,
|
ExplicitEnumRawValueRule.self,
|
||||||
ExplicitInitRule.self,
|
ExplicitInitRule.self,
|
||||||
|
ExplicitSelfRule.self,
|
||||||
ExplicitTopLevelACLRule.self,
|
ExplicitTopLevelACLRule.self,
|
||||||
ExplicitTypeInterfaceRule.self,
|
ExplicitTypeInterfaceRule.self,
|
||||||
ExtensionAccessModifierRule.self,
|
ExtensionAccessModifierRule.self,
|
||||||
|
|
|
@ -8,6 +8,7 @@ public struct RuleDescription: Equatable {
|
||||||
public let corrections: [String: String]
|
public let corrections: [String: String]
|
||||||
public let deprecatedAliases: Set<String>
|
public let deprecatedAliases: Set<String>
|
||||||
public let minSwiftVersion: SwiftVersion
|
public let minSwiftVersion: SwiftVersion
|
||||||
|
public let requiresFileOnDisk: Bool
|
||||||
|
|
||||||
public var consoleDescription: String { return "\(name) (\(identifier)): \(description)" }
|
public var consoleDescription: String { return "\(name) (\(identifier)): \(description)" }
|
||||||
|
|
||||||
|
@ -19,7 +20,8 @@ public struct RuleDescription: Equatable {
|
||||||
minSwiftVersion: SwiftVersion = .three,
|
minSwiftVersion: SwiftVersion = .three,
|
||||||
nonTriggeringExamples: [String] = [], triggeringExamples: [String] = [],
|
nonTriggeringExamples: [String] = [], triggeringExamples: [String] = [],
|
||||||
corrections: [String: String] = [:],
|
corrections: [String: String] = [:],
|
||||||
deprecatedAliases: Set<String> = []) {
|
deprecatedAliases: Set<String> = [],
|
||||||
|
requiresFileOnDisk: Bool = false) {
|
||||||
self.identifier = identifier
|
self.identifier = identifier
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
|
@ -29,6 +31,7 @@ public struct RuleDescription: Equatable {
|
||||||
self.corrections = corrections
|
self.corrections = corrections
|
||||||
self.deprecatedAliases = deprecatedAliases
|
self.deprecatedAliases = deprecatedAliases
|
||||||
self.minSwiftVersion = minSwiftVersion
|
self.minSwiftVersion = minSwiftVersion
|
||||||
|
self.requiresFileOnDisk = requiresFileOnDisk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,16 @@ public protocol Rule {
|
||||||
init() // Rules need to be able to be initialized with default values
|
init() // Rules need to be able to be initialized with default values
|
||||||
init(configuration: Any) throws
|
init(configuration: Any) throws
|
||||||
|
|
||||||
|
func validate(file: File, compilerArguments: [String]) -> [StyleViolation]
|
||||||
func validate(file: File) -> [StyleViolation]
|
func validate(file: File) -> [StyleViolation]
|
||||||
func isEqualTo(_ rule: Rule) -> Bool
|
func isEqualTo(_ rule: Rule) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Rule {
|
extension Rule {
|
||||||
|
public func validate(file: File, compilerArguments: [String]) -> [StyleViolation] {
|
||||||
|
return validate(file: file)
|
||||||
|
}
|
||||||
|
|
||||||
public func isEqualTo(_ rule: Rule) -> Bool {
|
public func isEqualTo(_ rule: Rule) -> Bool {
|
||||||
return type(of: self).description == type(of: rule).description
|
return type(of: self).description == type(of: rule).description
|
||||||
}
|
}
|
||||||
|
@ -32,11 +37,32 @@ public protocol ConfigurationProviderRule: Rule {
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol CorrectableRule: Rule {
|
public protocol CorrectableRule: Rule {
|
||||||
|
func correct(file: File, compilerArguments: [String]) -> [Correction]
|
||||||
func correct(file: File) -> [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 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
|
// MARK: - ConfigurationProviderRule conformance to Configurable
|
||||||
|
|
||||||
public extension ConfigurationProviderRule {
|
public extension ConfigurationProviderRule {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -7,47 +7,19 @@ struct AutoCorrectCommand: CommandProtocol {
|
||||||
let function = "Automatically correct warnings and errors"
|
let function = "Automatically correct warnings and errors"
|
||||||
|
|
||||||
func run(_ options: AutoCorrectOptions) -> Result<(), CommandantError<()>> {
|
func run(_ options: AutoCorrectOptions) -> Result<(), CommandantError<()>> {
|
||||||
let configuration = Configuration(options: options)
|
switch options.visitor {
|
||||||
let cache = options.ignoreCache ? nil : LinterCache(configuration: configuration)
|
case let .success(visitor):
|
||||||
let indentWidth: Int
|
return Configuration(options: options).visitLintableFiles(with: visitor).flatMap { files in
|
||||||
var useTabs: Bool
|
if !options.quiet {
|
||||||
|
let pluralSuffix = { (collection: [Any]) -> String in
|
||||||
switch configuration.indentation {
|
return collection.count != 1 ? "s" : ""
|
||||||
case .tabs:
|
}
|
||||||
indentWidth = 4
|
queuedPrintError("Done correcting \(files.count) file\(pluralSuffix(files))!")
|
||||||
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" : ""
|
|
||||||
}
|
}
|
||||||
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 cachePath: String
|
||||||
let ignoreCache: Bool
|
let ignoreCache: Bool
|
||||||
let useTabs: Bool
|
let useTabs: Bool
|
||||||
|
let compilerLogPath: String
|
||||||
|
|
||||||
// swiftlint:disable line_length
|
// 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 {
|
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 { paths in
|
return { configurationFile in { useScriptInputFiles in { quiet in { forceExclude in { format in { cachePath in { ignoreCache in { useTabs in { compilerLogPath in { paths in
|
||||||
let allPaths: [String]
|
let allPaths: [String]
|
||||||
if !path.isEmpty {
|
if !path.isEmpty {
|
||||||
allPaths = [path]
|
allPaths = [path]
|
||||||
} else {
|
} else {
|
||||||
allPaths = paths
|
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<()>>> {
|
static func evaluate(_ mode: CommandMode) -> Result<AutoCorrectOptions, CommandantError<CommandantError<()>>> {
|
||||||
// swiftlint:enable line_length
|
|
||||||
return create
|
return create
|
||||||
<*> mode <| pathOption(action: "correct")
|
<*> mode <| pathOption(action: "correct")
|
||||||
<*> mode <| configOption
|
<*> mode <| configOption
|
||||||
|
@ -96,7 +69,54 @@ struct AutoCorrectOptions: OptionsProtocol {
|
||||||
<*> mode <| Option(key: "use-tabs",
|
<*> mode <| Option(key: "use-tabs",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
usage: "should use tabs over spaces when reformatting. Deprecated.")
|
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
|
// This should go last to avoid eating other args
|
||||||
<*> mode <| pathsArgument(action: "correct")
|
<*> 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)'")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,18 +130,19 @@ struct LintOptions: OptionsProtocol {
|
||||||
let cachePath: String
|
let cachePath: String
|
||||||
let ignoreCache: Bool
|
let ignoreCache: Bool
|
||||||
let enableAllRules: Bool
|
let enableAllRules: Bool
|
||||||
|
let compilerLogPath: String
|
||||||
|
|
||||||
// swiftlint:disable line_length
|
// 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 {
|
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 { paths in
|
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]
|
let allPaths: [String]
|
||||||
if !path.isEmpty {
|
if !path.isEmpty {
|
||||||
allPaths = [path]
|
allPaths = [path]
|
||||||
} else {
|
} else {
|
||||||
allPaths = paths
|
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<()>>> {
|
static func evaluate(_ mode: CommandMode) -> Result<LintOptions, CommandantError<CommandantError<()>>> {
|
||||||
|
@ -170,6 +171,8 @@ struct LintOptions: OptionsProtocol {
|
||||||
usage: "ignore cache when linting")
|
usage: "ignore cache when linting")
|
||||||
<*> mode <| Option(key: "enable-all-rules", defaultValue: false,
|
<*> mode <| Option(key: "enable-all-rules", defaultValue: false,
|
||||||
usage: "run all rules, even opt-in and disabled ones, ignoring `whitelist_rules`")
|
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
|
// This should go last to avoid eating other args
|
||||||
<*> mode <| pathsArgument(action: "lint")
|
<*> mode <| pathsArgument(action: "lint")
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,84 +48,92 @@ private func autoreleasepool(block: () -> Void) { block() }
|
||||||
|
|
||||||
extension Configuration {
|
extension Configuration {
|
||||||
|
|
||||||
func visitLintableFiles(paths: [String], action: String, useSTDIN: Bool = false,
|
func visitLintableFiles(with visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
|
||||||
quiet: Bool = false, useScriptInputFiles: Bool, forceExclude: Bool,
|
return getFiles(with: visitor)
|
||||||
cache: LinterCache? = nil, parallel: Bool = false,
|
.flatMap { groupFiles($0, visitor: visitor) }
|
||||||
visitorBlock: @escaping (Linter) -> Void) -> Result<[File], CommandantError<()>> {
|
.flatMap { visit(filesPerConfiguration: $0, visitor: visitor) }
|
||||||
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 }))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable function_parameter_count
|
private func groupFiles(_ files: [File],
|
||||||
fileprivate func getFiles(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, forceExclude: Bool,
|
visitor: LintableFilesVisitor) -> Result<[Configuration: [File]], CommandantError<()>> {
|
||||||
useScriptInputFiles: Bool) -> Result<[File], CommandantError<()>> {
|
if files.isEmpty {
|
||||||
if useSTDIN {
|
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()
|
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
|
||||||
if let stdinString = String(data: stdinData, encoding: .utf8) {
|
if let stdinString = String(data: stdinData, encoding: .utf8) {
|
||||||
return .success([File(contents: stdinString)])
|
return .success([File(contents: stdinString)])
|
||||||
}
|
}
|
||||||
return .failure(.usageError(description: "stdin isn't a UTF8-encoded string"))
|
return .failure(.usageError(description: "stdin isn't a UTF8-encoded string"))
|
||||||
} else if useScriptInputFiles {
|
} else if visitor.useScriptInputFiles {
|
||||||
return scriptInputFiles()
|
return scriptInputFiles()
|
||||||
}
|
}
|
||||||
if !quiet {
|
if !visitor.quiet {
|
||||||
let filesInfo = paths.isEmpty ? "in current working directory" : "at paths \(paths.joined(separator: ", "))"
|
let filesInfo: String
|
||||||
let message = "\(action) Swift files \(filesInfo)"
|
if visitor.paths.isEmpty {
|
||||||
queuedPrintError(message)
|
filesInfo = "in current working directory"
|
||||||
|
} else {
|
||||||
|
filesInfo = "at paths \(visitor.paths.joined(separator: ", "))"
|
||||||
|
}
|
||||||
|
|
||||||
|
queuedPrintError("\(visitor.action) Swift files \(filesInfo)")
|
||||||
}
|
}
|
||||||
return .success(paths.flatMap {
|
return .success(visitor.paths.flatMap {
|
||||||
self.lintableFiles(inPath: $0, forceExclude: forceExclude)
|
self.lintableFiles(inPath: $0, forceExclude: visitor.forceExclude)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_parameter_count
|
|
||||||
|
|
||||||
private static func rootPath(from paths: [String]) -> String? {
|
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 ~)
|
// 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,
|
func visitLintableFiles(options: LintOptions, cache: LinterCache? = nil,
|
||||||
visitorBlock: @escaping (Linter) -> Void) -> Result<[File], CommandantError<()>> {
|
visitorBlock: @escaping (Linter) -> Void) -> Result<[File], CommandantError<()>> {
|
||||||
return visitLintableFiles(paths: options.paths, action: "Linting", useSTDIN: options.useSTDIN,
|
let visitor = LintableFilesVisitor(paths: options.paths, action: "Linting", useSTDIN: options.useSTDIN,
|
||||||
quiet: options.quiet, useScriptInputFiles: options.useScriptInputFiles,
|
quiet: options.quiet, useScriptInputFiles: options.useScriptInputFiles,
|
||||||
forceExclude: options.forceExclude, cache: cache, parallel: true,
|
forceExclude: options.forceExclude, cache: cache, parallel: true,
|
||||||
visitorBlock: visitorBlock)
|
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
|
// MARK: AutoCorrect Command
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -142,7 +142,10 @@
|
||||||
8B01E50220A4349100C9233E /* FunctionParameterCountRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B01E4FF20A4340A00C9233E /* FunctionParameterCountRuleTests.swift */; };
|
8B01E50220A4349100C9233E /* FunctionParameterCountRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B01E4FF20A4340A00C9233E /* FunctionParameterCountRuleTests.swift */; };
|
||||||
8F2CC1CB20A6A070006ED34F /* FileNameConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CA20A6A070006ED34F /* FileNameConfiguration.swift */; };
|
8F2CC1CB20A6A070006ED34F /* FileNameConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CA20A6A070006ED34F /* FileNameConfiguration.swift */; };
|
||||||
8F2CC1CD20A6A189006ED34F /* FileNameRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2CC1CC20A6A189006ED34F /* FileNameRuleTests.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 */; };
|
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 */; };
|
8FC9F5111F4B8E48006826C1 /* IsDisjointRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC9F5101F4B8E48006826C1 /* IsDisjointRule.swift */; };
|
||||||
8FD216CC205584AF008ED13F /* CharacterSet+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD216CB205584AF008ED13F /* CharacterSet+SwiftLint.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedOptionalBindingRule.swift; sourceTree = "<group>"; };
|
||||||
|
@ -989,6 +995,7 @@
|
||||||
D4470D581EB6B4D1008A1B2E /* EmptyEnumArgumentsRule.swift */,
|
D4470D581EB6B4D1008A1B2E /* EmptyEnumArgumentsRule.swift */,
|
||||||
D47079AC1DFE2FA700027086 /* EmptyParametersRule.swift */,
|
D47079AC1DFE2FA700027086 /* EmptyParametersRule.swift */,
|
||||||
D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */,
|
D47079A61DFCEB2D00027086 /* EmptyParenthesesWithTrailingClosureRule.swift */,
|
||||||
|
8FC8523A2117BDDE0015269B /* ExplicitSelfRule.swift */,
|
||||||
D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */,
|
D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */,
|
||||||
E88DEA931B099C0900A66CB0 /* IdentifierNameRule.swift */,
|
E88DEA931B099C0900A66CB0 /* IdentifierNameRule.swift */,
|
||||||
D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */,
|
D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */,
|
||||||
|
@ -1310,6 +1317,7 @@
|
||||||
children = (
|
children = (
|
||||||
E802ECFF1C56A56000A35AE1 /* Benchmark.swift */,
|
E802ECFF1C56A56000A35AE1 /* Benchmark.swift */,
|
||||||
E81FB3E31C6D507B00DC988F /* CommonOptions.swift */,
|
E81FB3E31C6D507B00DC988F /* CommonOptions.swift */,
|
||||||
|
8F6AA75A211905B8009BA28A /* LintableFilesVisitor.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1428,6 +1436,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E8B067801C13E49600E9E13F /* Configuration+CommandLine.swift */,
|
E8B067801C13E49600E9E13F /* Configuration+CommandLine.swift */,
|
||||||
|
8F6AA75C21190830009BA28A /* File+CompilerArguments.swift */,
|
||||||
E86E2B2D1E17443B001E823C /* Reporter+CommandLine.swift */,
|
E86E2B2D1E17443B001E823C /* Reporter+CommandLine.swift */,
|
||||||
6C032EF02027F79F00CD7E8D /* shim.swift */,
|
6C032EF02027F79F00CD7E8D /* shim.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1933,6 +1942,7 @@
|
||||||
B89F3BCF1FD5EE1400931E59 /* RequiredEnumCaseRuleConfiguration.swift in Sources */,
|
B89F3BCF1FD5EE1400931E59 /* RequiredEnumCaseRuleConfiguration.swift in Sources */,
|
||||||
D48B51211F4F5DEF0068AB98 /* RuleList+Documentation.swift in Sources */,
|
D48B51211F4F5DEF0068AB98 /* RuleList+Documentation.swift in Sources */,
|
||||||
8FC9F5111F4B8E48006826C1 /* IsDisjointRule.swift in Sources */,
|
8FC9F5111F4B8E48006826C1 /* IsDisjointRule.swift in Sources */,
|
||||||
|
8FC8523B2117BDDE0015269B /* ExplicitSelfRule.swift in Sources */,
|
||||||
4DCB8E7F1CBE494E0070FCF0 /* RegexHelpers.swift in Sources */,
|
4DCB8E7F1CBE494E0070FCF0 /* RegexHelpers.swift in Sources */,
|
||||||
E86396C21BADAAE5002C9E88 /* Reporter.swift in Sources */,
|
E86396C21BADAAE5002C9E88 /* Reporter.swift in Sources */,
|
||||||
A1A6F3F21EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift in Sources */,
|
A1A6F3F21EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift in Sources */,
|
||||||
|
@ -2021,6 +2031,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E86E2B2E1E17443B001E823C /* Reporter+CommandLine.swift in Sources */,
|
E86E2B2E1E17443B001E823C /* Reporter+CommandLine.swift in Sources */,
|
||||||
|
8F6AA75D21190830009BA28A /* File+CompilerArguments.swift in Sources */,
|
||||||
E8B067811C13E49600E9E13F /* Configuration+CommandLine.swift in Sources */,
|
E8B067811C13E49600E9E13F /* Configuration+CommandLine.swift in Sources */,
|
||||||
E802ED001C56A56000A35AE1 /* Benchmark.swift in Sources */,
|
E802ED001C56A56000A35AE1 /* Benchmark.swift in Sources */,
|
||||||
E83A0B351A5D382B0041A60A /* VersionCommand.swift in Sources */,
|
E83A0B351A5D382B0041A60A /* VersionCommand.swift in Sources */,
|
||||||
|
@ -2031,6 +2042,7 @@
|
||||||
6C032EF12027F79F00CD7E8D /* shim.swift in Sources */,
|
6C032EF12027F79F00CD7E8D /* shim.swift in Sources */,
|
||||||
D4DA1DFC1E19CD300037413D /* GenerateDocsCommand.swift in Sources */,
|
D4DA1DFC1E19CD300037413D /* GenerateDocsCommand.swift in Sources */,
|
||||||
E84E07471C13F95300F11122 /* AutoCorrectCommand.swift in Sources */,
|
E84E07471C13F95300F11122 /* AutoCorrectCommand.swift in Sources */,
|
||||||
|
8F6AA75B211905B8009BA28A /* LintableFilesVisitor.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -332,6 +332,12 @@ extension ExplicitInitRuleTests {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ExplicitSelfRuleTests {
|
||||||
|
static var allTests: [(String, (ExplicitSelfRuleTests) -> () throws -> Void)] = [
|
||||||
|
("testWithDefaultConfiguration", testWithDefaultConfiguration)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
extension ExplicitTopLevelACLRuleTests {
|
extension ExplicitTopLevelACLRuleTests {
|
||||||
static var allTests: [(String, (ExplicitTopLevelACLRuleTests) -> () throws -> Void)] = [
|
static var allTests: [(String, (ExplicitTopLevelACLRuleTests) -> () throws -> Void)] = [
|
||||||
("testWithDefaultConfiguration", testWithDefaultConfiguration)
|
("testWithDefaultConfiguration", testWithDefaultConfiguration)
|
||||||
|
@ -1274,6 +1280,7 @@ XCTMain([
|
||||||
testCase(ExplicitACLRuleTests.allTests),
|
testCase(ExplicitACLRuleTests.allTests),
|
||||||
testCase(ExplicitEnumRawValueRuleTests.allTests),
|
testCase(ExplicitEnumRawValueRuleTests.allTests),
|
||||||
testCase(ExplicitInitRuleTests.allTests),
|
testCase(ExplicitInitRuleTests.allTests),
|
||||||
|
testCase(ExplicitSelfRuleTests.allTests),
|
||||||
testCase(ExplicitTopLevelACLRuleTests.allTests),
|
testCase(ExplicitTopLevelACLRuleTests.allTests),
|
||||||
testCase(ExplicitTypeInterfaceConfigurationTests.allTests),
|
testCase(ExplicitTypeInterfaceConfigurationTests.allTests),
|
||||||
testCase(ExplicitTypeInterfaceRuleTests.allTests),
|
testCase(ExplicitTypeInterfaceRuleTests.allTests),
|
||||||
|
|
|
@ -174,6 +174,12 @@ class ExplicitInitRuleTests: XCTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExplicitSelfRuleTests: XCTestCase {
|
||||||
|
func testWithDefaultConfiguration() {
|
||||||
|
verifyRule(ExplicitSelfRule.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ExplicitTopLevelACLRuleTests: XCTestCase {
|
class ExplicitTopLevelACLRuleTests: XCTestCase {
|
||||||
func testWithDefaultConfiguration() {
|
func testWithDefaultConfiguration() {
|
||||||
verifyRule(ExplicitTopLevelACLRule.description)
|
verifyRule(ExplicitTopLevelACLRule.description)
|
||||||
|
|
|
@ -13,11 +13,41 @@ extension String {
|
||||||
|
|
||||||
let allRuleIdentifiers = Array(masterRuleList.list.keys)
|
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()
|
File.clearCaches()
|
||||||
let stringStrippingMarkers = string.replacingOccurrences(of: violationMarker, with: "")
|
let stringStrippingMarkers = string.replacingOccurrences(of: violationMarker, with: "")
|
||||||
let file = File(contents: stringStrippingMarkers)
|
guard requiresFileOnDisk else {
|
||||||
return Linter(file: file, configuration: config).styleViolations
|
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]) {
|
private func cleanedContentsAndMarkerOffsets(from contents: String) -> (String, [Int]) {
|
||||||
|
@ -65,10 +95,9 @@ private func render(locations: [Location], in contents: String) -> String {
|
||||||
|
|
||||||
private extension Configuration {
|
private extension Configuration {
|
||||||
func assertCorrection(_ before: String, expected: String) {
|
func assertCorrection(_ before: String, expected: String) {
|
||||||
guard let path = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
guard let path = temporaryFileURL()?.path else {
|
||||||
.appendingPathComponent(NSUUID().uuidString + ".swift")?.path else {
|
XCTFail("couldn't generate temporary path for assertCorrection()")
|
||||||
XCTFail("couldn't generate temporary path for assertCorrection()")
|
return
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let (cleanedBefore, markerOffsets) = cleanedContentsAndMarkerOffsets(from: before)
|
let (cleanedBefore, markerOffsets) = cleanedContentsAndMarkerOffsets(from: before)
|
||||||
do {
|
do {
|
||||||
|
@ -83,7 +112,7 @@ private extension Configuration {
|
||||||
}
|
}
|
||||||
// expectedLocations are needed to create before call `correct()`
|
// expectedLocations are needed to create before call `correct()`
|
||||||
let expectedLocations = markerOffsets.map { Location(file: file, characterOffset: $0) }
|
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
|
$0.location < $1.location
|
||||||
}
|
}
|
||||||
if expectedLocations.isEmpty {
|
if expectedLocations.isEmpty {
|
||||||
|
@ -152,6 +181,10 @@ extension XCTestCase {
|
||||||
skipDisableCommandTests: Bool = false,
|
skipDisableCommandTests: Bool = false,
|
||||||
testMultiByteOffsets: Bool = true,
|
testMultiByteOffsets: Bool = true,
|
||||||
testShebang: Bool = true) {
|
testShebang: Bool = true) {
|
||||||
|
guard SwiftVersion.current >= ruleDescription.minSwiftVersion else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard let config = makeConfig(ruleConfiguration,
|
guard let config = makeConfig(ruleConfiguration,
|
||||||
ruleDescription.identifier,
|
ruleDescription.identifier,
|
||||||
skipDisableCommandTests: skipDisableCommandTests) else {
|
skipDisableCommandTests: skipDisableCommandTests) else {
|
||||||
|
@ -159,40 +192,6 @@ extension XCTestCase {
|
||||||
return
|
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]
|
let disableCommands: [String]
|
||||||
if skipDisableCommandTests {
|
if skipDisableCommandTests {
|
||||||
disableCommands = []
|
disableCommands = []
|
||||||
|
@ -200,18 +199,75 @@ extension XCTestCase {
|
||||||
disableCommands = ruleDescription.allIdentifiers.map { "// swiftlint:disable \($0)\n" }
|
disableCommands = ruleDescription.allIdentifiers.map { "// swiftlint:disable \($0)\n" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// "disable" commands doesn't violate
|
self.verifyLint(ruleDescription, config: config, commentDoesntViolate: commentDoesntViolate,
|
||||||
for command in disableCommands {
|
stringDoesntViolate: stringDoesntViolate, skipCommentTests: skipCommentTests,
|
||||||
XCTAssert(triggers.flatMap({ violations(command + $0, config: config) }).isEmpty)
|
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
|
// corrections
|
||||||
ruleDescription.corrections.forEach {
|
ruleDescription.corrections.forEach {
|
||||||
testCorrection($0, configuration: config, testMultiByteOffsets: testMultiByteOffsets)
|
testCorrection($0, configuration: config, testMultiByteOffsets: testMultiByteOffsets)
|
||||||
}
|
}
|
||||||
// make sure strings that don't trigger aren't corrected
|
// make sure strings that don't trigger aren't corrected
|
||||||
zip(nonTriggers, nonTriggers).forEach {
|
ruleDescription.nonTriggeringExamples.forEach {
|
||||||
testCorrection($0, configuration: config, testMultiByteOffsets: testMultiByteOffsets)
|
testCorrection(($0, $0), configuration: config, testMultiByteOffsets: testMultiByteOffsets)
|
||||||
}
|
}
|
||||||
|
|
||||||
// "disable" commands do not correct
|
// "disable" commands do not correct
|
||||||
|
@ -222,14 +278,14 @@ extension XCTestCase {
|
||||||
config.assertCorrection(expectedCleaned, expected: expectedCleaned)
|
config.assertCorrection(expectedCleaned, expected: expectedCleaned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func verifyExamples(triggers: [String], nonTriggers: [String],
|
private func verifyExamples(triggers: [String], nonTriggers: [String],
|
||||||
configuration config: Configuration) {
|
configuration config: Configuration, requiresFileOnDisk: Bool) {
|
||||||
// Non-triggering examples don't violate
|
// Non-triggering examples don't violate
|
||||||
for nonTrigger in nonTriggers {
|
for nonTrigger in nonTriggers {
|
||||||
let unexpectedViolations = violations(nonTrigger, config: config)
|
let unexpectedViolations = violations(nonTrigger, config: config,
|
||||||
|
requiresFileOnDisk: requiresFileOnDisk)
|
||||||
if unexpectedViolations.isEmpty { continue }
|
if unexpectedViolations.isEmpty { continue }
|
||||||
let nonTriggerWithViolations = render(violations: unexpectedViolations, in: nonTrigger)
|
let nonTriggerWithViolations = render(violations: unexpectedViolations, in: nonTrigger)
|
||||||
XCTFail("nonTriggeringExample violated: \n\(nonTriggerWithViolations)")
|
XCTFail("nonTriggeringExample violated: \n\(nonTriggerWithViolations)")
|
||||||
|
@ -237,7 +293,8 @@ extension XCTestCase {
|
||||||
|
|
||||||
// Triggering examples violate
|
// Triggering examples violate
|
||||||
for trigger in triggers {
|
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
|
// Triggering examples with violation markers violate at the marker's location
|
||||||
let (cleanTrigger, markerOffsets) = cleanedContentsAndMarkerOffsets(from: trigger)
|
let (cleanTrigger, markerOffsets) = cleanedContentsAndMarkerOffsets(from: trigger)
|
||||||
|
|
|
@ -84,6 +84,6 @@ class TrailingCommaRuleTests: XCTestCase {
|
||||||
|
|
||||||
private func trailingCommaViolations(_ string: String, ruleConfiguration: Any? = nil) -> [StyleViolation] {
|
private func trailingCommaViolations(_ string: String, ruleConfiguration: Any? = nil) -> [StyleViolation] {
|
||||||
let config = makeConfig(ruleConfiguration, TrailingCommaRule.description.identifier)!
|
let config = makeConfig(ruleConfiguration, TrailingCommaRule.description.identifier)!
|
||||||
return SwiftLintFrameworkTests.violations(string, config: config)
|
return violations(string, config: config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue