Compare commits
19 Commits
main
...
unused-dec
Author | SHA1 | Date |
---|---|---|
![]() |
61ed3d7ea9 | |
![]() |
46959cbd1a | |
![]() |
03398f4918 | |
![]() |
69c404c863 | |
![]() |
816785f8a0 | |
![]() |
68e8b2b526 | |
![]() |
c823057ed3 | |
![]() |
d5b6b65c73 | |
![]() |
f0444d67ba | |
![]() |
66f13bfc26 | |
![]() |
5f513eea45 | |
![]() |
e95b8c4e22 | |
![]() |
45d94c017a | |
![]() |
f0855fcbec | |
![]() |
8569dd5df8 | |
![]() |
aa8bc029cf | |
![]() |
de5651d806 | |
![]() |
ba8bfcae22 | |
![]() |
b6535eb73e |
|
@ -4,8 +4,9 @@ included:
|
|||
excluded:
|
||||
- Tests/SwiftLintFrameworkTests/Resources
|
||||
analyzer_rules:
|
||||
- unused_import
|
||||
- unused_private_declaration
|
||||
- unused_declaration
|
||||
# - unused_import
|
||||
# - unused_private_declaration
|
||||
opt_in_rules:
|
||||
- anyobject_protocol
|
||||
- array_init
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
|
||||
#### Experimental
|
||||
|
||||
* None.
|
||||
* Add a two-stage `CollectingRule` protocol to support rules that collect data
|
||||
from all files before validating. Collecting rules implement a `collect`
|
||||
method which is called once for every file, before _any_ file is checked for
|
||||
violations. By collecting, rules can be written which validate across
|
||||
multiple files for things like unused declarations.
|
||||
[Elliott Williams](https://github.com/elliottwilliams)
|
||||
[#2431](https://github.com/realm/SwiftLint/issues/2431)
|
||||
|
||||
#### Enhancements
|
||||
|
||||
|
|
4
Makefile
4
Makefile
|
@ -62,8 +62,8 @@ test_tsan:
|
|||
swift build --build-tests $(TSAN_SWIFT_BUILD_FLAGS)
|
||||
DYLD_INSERT_LIBRARIES=$(TSAN_LIB) $(TSAN_XCTEST) $(TSAN_TEST_BUNDLE)
|
||||
|
||||
write_xcodebuild_log: bootstrap
|
||||
xcodebuild -workspace SwiftLint.xcworkspace -scheme swiftlint > xcodebuild.log
|
||||
write_xcodebuild_log:
|
||||
xcodebuild -workspace SwiftLint.xcworkspace -scheme swiftlint clean build-for-testing > xcodebuild.log
|
||||
|
||||
analyze: write_xcodebuild_log
|
||||
swift run -c release swiftlint analyze --strict --compiler-log-path xcodebuild.log
|
||||
|
|
|
@ -39,11 +39,11 @@
|
|||
},
|
||||
{
|
||||
"package": "SourceKitten",
|
||||
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
|
||||
"repositoryURL": "https://github.com/cltnschlosser/SourceKitten.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "fd9091759201473aa234c22322a3939615aef59a",
|
||||
"version": "0.23.2"
|
||||
"branch": "cs_sourceKitMemoryLeak2",
|
||||
"revision": "b6c9c6c56a898faabe2671152dfa963bb8d6f5e9",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ let package = Package(
|
|||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/Carthage/Commandant.git", .upToNextMinor(from: "0.16.0")),
|
||||
.package(url: "https://github.com/jpsim/SourceKitten.git", from: "0.23.2"),
|
||||
.package(url: "https://github.com/cltnschlosser/SourceKitten.git", .branch("cs_sourceKitMemoryLeak2")),
|
||||
.package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
||||
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.0.0")] : []),
|
||||
|
|
|
@ -45,11 +45,11 @@ extension Array {
|
|||
return (copy[0..<pivot], copy[pivot..<count])
|
||||
}
|
||||
|
||||
func parallelFlatMap<T>(transform: @escaping ((Element) -> [T])) -> [T] {
|
||||
func parallelFlatMap<T>(transform: (Element) -> [T]) -> [T] {
|
||||
return parallelMap(transform: transform).flatMap { $0 }
|
||||
}
|
||||
|
||||
func parallelCompactMap<T>(transform: @escaping ((Element) -> T?)) -> [T] {
|
||||
func parallelCompactMap<T>(transform: (Element) -> T?) -> [T] {
|
||||
return parallelMap(transform: transform).compactMap { $0 }
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ private extension Rule {
|
|||
// As we need the configuration to get custom identifiers.
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func lint(file: File, regions: [Region], benchmark: Bool,
|
||||
storage: RuleStorage,
|
||||
configuration: Configuration,
|
||||
superfluousDisableCommandRule: SuperfluousDisableCommandRule?,
|
||||
compilerArguments: [String]) -> LintResult? {
|
||||
|
@ -63,10 +64,10 @@ private extension Rule {
|
|||
let ruleTime: (String, Double)?
|
||||
if benchmark {
|
||||
let start = Date()
|
||||
violations = validate(file: file, compilerArguments: compilerArguments)
|
||||
violations = validate(file: file, using: storage, compilerArguments: compilerArguments)
|
||||
ruleTime = (ruleID, -start.timeIntervalSinceNow)
|
||||
} else {
|
||||
violations = validate(file: file, compilerArguments: compilerArguments)
|
||||
violations = validate(file: file, using: storage, compilerArguments: compilerArguments)
|
||||
ruleTime = nil
|
||||
}
|
||||
|
||||
|
@ -107,22 +108,70 @@ private extension Rule {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents a file that can be linted for style violations and corrections after being collected.
|
||||
public struct Linter {
|
||||
public let file: File
|
||||
public var isCollecting: Bool
|
||||
fileprivate let rules: [Rule]
|
||||
fileprivate let cache: LinterCache?
|
||||
fileprivate let configuration: Configuration
|
||||
fileprivate let compilerArguments: [String]
|
||||
|
||||
public init(file: File, configuration: Configuration = Configuration()!, cache: LinterCache? = nil,
|
||||
compilerArguments: [String] = []) {
|
||||
self.file = file
|
||||
self.cache = cache
|
||||
self.configuration = configuration
|
||||
self.compilerArguments = compilerArguments
|
||||
let rules = configuration.rules.filter { rule in
|
||||
if compilerArguments.isEmpty {
|
||||
return !(rule is AnalyzerRule)
|
||||
} else {
|
||||
return rule is AnalyzerRule
|
||||
}
|
||||
}
|
||||
self.rules = rules
|
||||
self.isCollecting = rules.contains(where: { $0 is AnyCollectingRule })
|
||||
}
|
||||
|
||||
/// Returns a linter capable of checking for violations after running each rule's collection step.
|
||||
public func collect(into storage: RuleStorage) -> CollectedLinter {
|
||||
DispatchQueue.concurrentPerform(iterations: rules.count) { idx in
|
||||
rules[idx].collectInfo(for: file, into: storage, compilerArguments: compilerArguments)
|
||||
}
|
||||
return CollectedLinter(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a file that can compute style violations and corrections for a list of rules.
|
||||
///
|
||||
/// A `CollectedLinter` is only created after a `Linter` has run its collection steps in `Linter.collect(into:)`.
|
||||
public struct CollectedLinter {
|
||||
public let file: File
|
||||
private let rules: [Rule]
|
||||
private let cache: LinterCache?
|
||||
private let configuration: Configuration
|
||||
private let compilerArguments: [String]
|
||||
|
||||
public var styleViolations: [StyleViolation] {
|
||||
return getStyleViolations().0
|
||||
fileprivate init(from linter: Linter) {
|
||||
file = linter.file
|
||||
rules = linter.rules
|
||||
cache = linter.cache
|
||||
configuration = linter.configuration
|
||||
compilerArguments = linter.compilerArguments
|
||||
}
|
||||
|
||||
public var styleViolationsAndRuleTimes: ([StyleViolation], [(id: String, time: Double)]) {
|
||||
return getStyleViolations(benchmark: true)
|
||||
public func styleViolations(using storage: RuleStorage) -> [StyleViolation] {
|
||||
return getStyleViolations(using: storage).0
|
||||
}
|
||||
|
||||
private func getStyleViolations(benchmark: Bool = false) -> ([StyleViolation], [(id: String, time: Double)]) {
|
||||
public func styleViolationsAndRuleTimes(using storage: RuleStorage)
|
||||
-> ([StyleViolation], [(id: String, time: Double)]) {
|
||||
return getStyleViolations(using: storage, benchmark: true)
|
||||
}
|
||||
|
||||
private func getStyleViolations(using storage: RuleStorage,
|
||||
benchmark: Bool = false) -> ([StyleViolation], [(id: String, time: Double)]) {
|
||||
if let cached = cachedStyleViolations(benchmark: benchmark) {
|
||||
return cached
|
||||
}
|
||||
|
@ -136,6 +185,7 @@ public struct Linter {
|
|||
}) as? SuperfluousDisableCommandRule
|
||||
let validationResults = rules.parallelCompactMap {
|
||||
$0.lint(file: self.file, regions: regions, benchmark: benchmark,
|
||||
storage: storage,
|
||||
configuration: self.configuration,
|
||||
superfluousDisableCommandRule: superfluousDisableCommandRule,
|
||||
compilerArguments: self.compilerArguments)
|
||||
|
@ -184,29 +234,14 @@ public struct Linter {
|
|||
return (cachedViolations, ruleTimes)
|
||||
}
|
||||
|
||||
public init(file: File, configuration: Configuration = Configuration()!, cache: LinterCache? = nil,
|
||||
compilerArguments: [String] = []) {
|
||||
self.file = file
|
||||
self.configuration = configuration
|
||||
self.cache = cache
|
||||
self.compilerArguments = compilerArguments
|
||||
self.rules = configuration.rules.filter { rule in
|
||||
if compilerArguments.isEmpty {
|
||||
return !(rule is AnalyzerRule)
|
||||
} else {
|
||||
return rule is AnalyzerRule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func correct() -> [Correction] {
|
||||
public func correct(using storage: RuleStorage) -> [Correction] {
|
||||
if let violations = cachedStyleViolations()?.0, violations.isEmpty {
|
||||
return []
|
||||
}
|
||||
|
||||
var corrections = [Correction]()
|
||||
for rule in rules.compactMap({ $0 as? CorrectableRule }) {
|
||||
let newCorrections = rule.correct(file: file, compilerArguments: compilerArguments)
|
||||
let newCorrections = rule.correct(file: file, using: storage, compilerArguments: compilerArguments)
|
||||
corrections += newCorrections
|
||||
if !newCorrections.isEmpty {
|
||||
file.invalidateCache()
|
||||
|
|
|
@ -162,6 +162,7 @@ public let masterRuleList = RuleList(rules: [
|
|||
UnusedCaptureListRule.self,
|
||||
UnusedClosureParameterRule.self,
|
||||
UnusedControlFlowLabelRule.self,
|
||||
UnusedDeclarationRule.self,
|
||||
UnusedEnumeratedRule.self,
|
||||
UnusedImportRule.self,
|
||||
UnusedOptionalBindingRule.self,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import Dispatch
|
||||
import SourceKittenFramework
|
||||
|
||||
public class RuleStorage {
|
||||
private var storage: [ObjectIdentifier: [File: Any]]
|
||||
private let access = DispatchQueue(label: "io.realm.swiftlint.ruleStorageAccess", attributes: .concurrent)
|
||||
|
||||
public init() {
|
||||
storage = [:]
|
||||
}
|
||||
|
||||
func collect<R: CollectingRule>(info: R.FileInfo, for file: File, in rule: R) {
|
||||
let key = ObjectIdentifier(R.self)
|
||||
access.sync(flags: .barrier) {
|
||||
storage[key, default: [:]][file] = info
|
||||
}
|
||||
}
|
||||
|
||||
func collectedInfo<R: CollectingRule>(for rule: R) -> [File: R.FileInfo]? {
|
||||
return access.sync {
|
||||
storage[ObjectIdentifier(R.self)] as? [File: R.FileInfo]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,9 +11,17 @@ public protocol Rule {
|
|||
func validate(file: File, compilerArguments: [String]) -> [StyleViolation]
|
||||
func validate(file: File) -> [StyleViolation]
|
||||
func isEqualTo(_ rule: Rule) -> Bool
|
||||
|
||||
// These are called by the linter and are always implemented in extensions.
|
||||
func collectInfo(for file: File, into storage: RuleStorage, compilerArguments: [String])
|
||||
func validate(file: File, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation]
|
||||
}
|
||||
|
||||
extension Rule {
|
||||
public func validate(file: File, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation] {
|
||||
return validate(file: file, compilerArguments: compilerArguments)
|
||||
}
|
||||
|
||||
public func validate(file: File, compilerArguments: [String]) -> [StyleViolation] {
|
||||
return validate(file: file)
|
||||
}
|
||||
|
@ -22,6 +30,10 @@ extension Rule {
|
|||
return type(of: self).description == type(of: rule).description
|
||||
}
|
||||
|
||||
public func collectInfo(for file: File, into storage: RuleStorage, compilerArguments: [String]) {
|
||||
// no-op: only CollectingRules mutate their storage
|
||||
}
|
||||
|
||||
internal var cacheDescription: String {
|
||||
return (self as? CacheDescriptionProvider)?.cacheDescription ?? configurationDescription
|
||||
}
|
||||
|
@ -40,12 +52,18 @@ public protocol ConfigurationProviderRule: Rule {
|
|||
public protocol CorrectableRule: Rule {
|
||||
func correct(file: File, compilerArguments: [String]) -> [Correction]
|
||||
func correct(file: File) -> [Correction]
|
||||
|
||||
// Called by the linter and are always implemented in extensions.
|
||||
func correct(file: File, using storage: RuleStorage, compilerArguments: [String]) -> [Correction]
|
||||
}
|
||||
|
||||
public extension CorrectableRule {
|
||||
func correct(file: File, compilerArguments: [String]) -> [Correction] {
|
||||
return correct(file: file)
|
||||
}
|
||||
func correct(file: File, using storage: RuleStorage, compilerArguments: [String]) -> [Correction] {
|
||||
return correct(file: file, compilerArguments: compilerArguments)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol SubstitutionCorrectableRule: CorrectableRule {
|
||||
|
@ -117,7 +135,96 @@ public extension AnalyzerRule where Self: CorrectableRule {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - ConfigurationProviderRule conformance to Configurable
|
||||
// MARK: - Collecting rules
|
||||
|
||||
/// Type-erased protocol used to check whether a rule is collectable.
|
||||
public protocol AnyCollectingRule: Rule { }
|
||||
|
||||
public protocol CollectingRule: AnyCollectingRule {
|
||||
associatedtype FileInfo
|
||||
func collectInfo(for file: File, compilerArguments: [String]) -> FileInfo
|
||||
func collectInfo(for file: File) -> FileInfo
|
||||
func validate(file: File, collectedInfo: [File: FileInfo], compilerArguments: [String]) -> [StyleViolation]
|
||||
func validate(file: File, collectedInfo: [File: FileInfo]) -> [StyleViolation]
|
||||
}
|
||||
|
||||
public extension CollectingRule {
|
||||
func collectInfo(for file: File, into storage: RuleStorage, compilerArguments: [String]) {
|
||||
storage.collect(info: collectInfo(for: file, compilerArguments: compilerArguments),
|
||||
for: file, in: self)
|
||||
}
|
||||
func validate(file: File, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation] {
|
||||
guard let info = storage.collectedInfo(for: self) else {
|
||||
queuedFatalError("Attempt to validate a CollectingRule before collecting info for it")
|
||||
}
|
||||
return validate(file: file, collectedInfo: info, compilerArguments: compilerArguments)
|
||||
}
|
||||
func collectInfo(for file: File, compilerArguments: [String]) -> FileInfo {
|
||||
return collectInfo(for: file)
|
||||
}
|
||||
func validate(file: File, collectedInfo: [File: FileInfo], compilerArguments: [String]) -> [StyleViolation] {
|
||||
return validate(file: file, collectedInfo: collectedInfo)
|
||||
}
|
||||
func validate(file: File) -> [StyleViolation] {
|
||||
queuedFatalError("Must call `validate(file:collectedInfo:)` for CollectingRule")
|
||||
}
|
||||
func validate(file: File, compilerArguments: [String]) -> [StyleViolation] {
|
||||
queuedFatalError("Must call `validate(file:collectedInfo:compilerArguments:)` for CollectingRule")
|
||||
}
|
||||
}
|
||||
|
||||
public extension CollectingRule where Self: AnalyzerRule {
|
||||
func collectInfo(for file: File) -> FileInfo {
|
||||
queuedFatalError(
|
||||
"Must call `collect(infoFor:compilerArguments:)` for AnalyzerRule & CollectingRule"
|
||||
)
|
||||
}
|
||||
func validate(file: File) -> [StyleViolation] {
|
||||
queuedFatalError(
|
||||
"Must call `validate(file:collectedInfo:compilerArguments:)` for AnalyzerRule & CollectingRule"
|
||||
)
|
||||
}
|
||||
func validate(file: File, collectedInfo: [File: FileInfo]) -> [StyleViolation] {
|
||||
queuedFatalError(
|
||||
"Must call `validate(file:collectedInfo:compilerArguments:)` for AnalyzerRule & CollectingRule"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CollectingCorrectableRule: CollectingRule, CorrectableRule {
|
||||
func correct(file: File, collectedInfo: [File: FileInfo], compilerArguments: [String]) -> [Correction]
|
||||
func correct(file: File, collectedInfo: [File: FileInfo]) -> [Correction]
|
||||
}
|
||||
|
||||
public extension CollectingCorrectableRule {
|
||||
func correct(file: File, collectedInfo: [File: FileInfo], compilerArguments: [String]) -> [Correction] {
|
||||
return correct(file: file, collectedInfo: collectedInfo)
|
||||
}
|
||||
func correct(file: File, using storage: RuleStorage, compilerArguments: [String]) -> [Correction] {
|
||||
guard let info = storage.collectedInfo(for: self) else {
|
||||
queuedFatalError("Attempt to correct a CollectingRule before collecting info for it")
|
||||
}
|
||||
return correct(file: file, collectedInfo: info, compilerArguments: compilerArguments)
|
||||
}
|
||||
func correct(file: File) -> [Correction] {
|
||||
queuedFatalError("Must call `correct(file:collectedInfo:)` for AnalyzerRule")
|
||||
}
|
||||
func correct(file: File, compilerArguments: [String]) -> [Correction] {
|
||||
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
|
||||
}
|
||||
}
|
||||
|
||||
public extension CollectingCorrectableRule where Self: AnalyzerRule {
|
||||
func correct(file: File) -> [Correction] {
|
||||
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
|
||||
}
|
||||
func correct(file: File, compilerArguments: [String]) -> [Correction] {
|
||||
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
|
||||
}
|
||||
func correct(file: File, collectedInfo: [File: FileInfo]) -> [Correction] {
|
||||
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
|
||||
}
|
||||
}
|
||||
|
||||
public extension ConfigurationProviderRule {
|
||||
init(configuration: Any) throws {
|
||||
|
|
|
@ -5,12 +5,6 @@ public protocol RuleConfiguration {
|
|||
func isEqualTo(_ ruleConfiguration: RuleConfiguration) -> Bool
|
||||
}
|
||||
|
||||
extension RuleConfiguration {
|
||||
internal var cacheDescription: String {
|
||||
return (self as? CacheDescriptionProvider)?.cacheDescription ?? consoleDescription
|
||||
}
|
||||
}
|
||||
|
||||
public extension RuleConfiguration where Self: Equatable {
|
||||
func isEqualTo(_ ruleConfiguration: RuleConfiguration) -> Bool {
|
||||
return self == ruleConfiguration as? Self
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
import Foundation
|
||||
import SourceKittenFramework
|
||||
|
||||
public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProviderRule, AnalyzerRule, CollectingRule {
|
||||
public struct FileUSRs {
|
||||
var referenced: Set<String>
|
||||
var declared: [(usr: String, nameOffset: Int)]
|
||||
var testCaseUSRs: Set<String>
|
||||
}
|
||||
|
||||
public typealias FileInfo = FileUSRs
|
||||
|
||||
public var configuration = UnusedDeclarationConfiguration(severity: .error, includePublicAndOpen: false)
|
||||
|
||||
public init() {}
|
||||
|
||||
public static let description = RuleDescription(
|
||||
identifier: "unused_declaration",
|
||||
name: "Unused Declaration",
|
||||
description: "Declarations should be referenced at least once within all files linted.",
|
||||
kind: .lint,
|
||||
nonTriggeringExamples: [
|
||||
"""
|
||||
let kConstant = 0
|
||||
_ = kConstant
|
||||
""",
|
||||
"""
|
||||
struct Item {}
|
||||
struct ResponseModel: Codable {
|
||||
let items: [Item]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case items = "ResponseItems"
|
||||
}
|
||||
}
|
||||
|
||||
_ = ResponseModel(items: [Item()]).items
|
||||
""",
|
||||
"""
|
||||
class ResponseModel {
|
||||
@objc func foo() {
|
||||
}
|
||||
}
|
||||
_ = ResponseModel()
|
||||
"""
|
||||
],
|
||||
triggeringExamples: [
|
||||
"""
|
||||
let ↓kConstant = 0
|
||||
""",
|
||||
"""
|
||||
struct ↓ResponseModel: Codable {
|
||||
let ↓items: [Item]
|
||||
|
||||
enum ↓CodingKeys: String {
|
||||
case items = "ResponseItems"
|
||||
}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
class ↓ResponseModel {
|
||||
func ↓foo() {
|
||||
}
|
||||
}
|
||||
"""
|
||||
],
|
||||
requiresFileOnDisk: true
|
||||
)
|
||||
|
||||
public func collectInfo(for file: File, compilerArguments: [String]) -> UnusedDeclarationRule.FileUSRs {
|
||||
guard !compilerArguments.isEmpty else {
|
||||
queuedPrintError("""
|
||||
Attempted to lint file at path '\(file.path ?? "...")' with the \
|
||||
\(type(of: self).description.identifier) rule without any compiler arguments.
|
||||
""")
|
||||
return FileUSRs(referenced: [], declared: [], testCaseUSRs: [])
|
||||
}
|
||||
|
||||
let allCursorInfo = file.allCursorInfo(compilerArguments: compilerArguments)
|
||||
return FileUSRs(referenced: Set(File.referencedUSRs(allCursorInfo: allCursorInfo)),
|
||||
declared: File.declaredUSRs(allCursorInfo: allCursorInfo,
|
||||
includePublicAndOpen: configuration.includePublicAndOpen),
|
||||
testCaseUSRs: File.testCaseUSRs(allCursorInfo: allCursorInfo))
|
||||
}
|
||||
|
||||
public func validate(file: File, collectedInfo: [File: UnusedDeclarationRule.FileUSRs],
|
||||
compilerArguments: [String]) -> [StyleViolation] {
|
||||
let allReferencedUSRs = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.referenced) }
|
||||
let allTestCaseUSRs = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.testCaseUSRs) }
|
||||
return violationOffsets(in: file, compilerArguments: compilerArguments,
|
||||
declaredUSRs: collectedInfo[file]?.declared ?? [],
|
||||
allReferencedUSRs: allReferencedUSRs,
|
||||
allTestCaseUSRs: allTestCaseUSRs)
|
||||
.map {
|
||||
StyleViolation(ruleDescription: type(of: self).description,
|
||||
severity: configuration.severity,
|
||||
location: Location(file: file, byteOffset: $0))
|
||||
}
|
||||
}
|
||||
|
||||
private func violationOffsets(in file: File, compilerArguments: [String],
|
||||
declaredUSRs: [(usr: String, nameOffset: Int)],
|
||||
allReferencedUSRs: Set<String>,
|
||||
allTestCaseUSRs: Set<String>) -> [Int] {
|
||||
// Unused declarations are:
|
||||
// 1. all declarations
|
||||
// 2. minus all references
|
||||
// 3. minus all XCTestCase subclasses
|
||||
// 4. minus all XCTest test functions
|
||||
let unusedDeclarations = declaredUSRs
|
||||
.filter { !allReferencedUSRs.contains($0.usr) }
|
||||
.filter { !allTestCaseUSRs.contains($0.usr) }
|
||||
.filter { declaredUSR in
|
||||
return !allTestCaseUSRs.contains(where: { testCaseUSR in
|
||||
return declaredUSR.usr.hasPrefix(testCaseUSR + "(im)test") ||
|
||||
declaredUSR.usr.hasPrefix(
|
||||
testCaseUSR.replacingOccurrences(of: "@M@", with: "@CM@") + "(im)test"
|
||||
)
|
||||
})
|
||||
}
|
||||
return unusedDeclarations.map { $0.nameOffset }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File Extensions
|
||||
|
||||
private extension File {
|
||||
func allCursorInfo(compilerArguments: [String]) -> [[String: SourceKitRepresentable]] {
|
||||
guard let path = path, let editorOpen = try? Request.editorOpen(file: self).sendIfNotDisabled() else {
|
||||
return []
|
||||
}
|
||||
|
||||
return syntaxMap.tokens.compactMap { token in
|
||||
guard let kind = SyntaxKind(rawValue: token.type), !syntaxKindsToSkip.contains(kind) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let offset = Int64(token.offset)
|
||||
let request = Request.cursorInfo(file: path, offset: offset, arguments: compilerArguments)
|
||||
guard var cursorInfo = try? request.sendIfNotDisabled() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let acl = File.aclAtOffset(offset, substructureElement: editorOpen) {
|
||||
cursorInfo["key.accessibility"] = acl
|
||||
}
|
||||
cursorInfo["swiftlint.offset"] = offset
|
||||
return cursorInfo
|
||||
}
|
||||
}
|
||||
|
||||
static func declaredUSRs(allCursorInfo: [[String: SourceKitRepresentable]], includePublicAndOpen: Bool)
|
||||
-> [(usr: String, nameOffset: Int)] {
|
||||
return allCursorInfo.compactMap { cursorInfo in
|
||||
return declaredUSRAndOffset(cursorInfo: cursorInfo, includePublicAndOpen: includePublicAndOpen)
|
||||
}
|
||||
}
|
||||
|
||||
static func referencedUSRs(allCursorInfo: [[String: SourceKitRepresentable]]) -> [String] {
|
||||
return allCursorInfo.compactMap(referencedUSR)
|
||||
}
|
||||
|
||||
static func testCaseUSRs(allCursorInfo: [[String: SourceKitRepresentable]]) -> Set<String> {
|
||||
return Set(allCursorInfo.compactMap(testCaseUSR))
|
||||
}
|
||||
|
||||
private static func declaredUSRAndOffset(cursorInfo: [String: SourceKitRepresentable], includePublicAndOpen: Bool)
|
||||
-> (usr: String, nameOffset: Int)? {
|
||||
if let offset = cursorInfo["swiftlint.offset"] as? Int64,
|
||||
let usr = cursorInfo["key.usr"] as? String,
|
||||
let kind = (cursorInfo["key.kind"] as? String).flatMap(SwiftDeclarationKind.init(rawValue:)),
|
||||
!declarationKindsToSkip.contains(kind),
|
||||
let acl = (cursorInfo["key.accessibility"] as? String).flatMap(AccessControlLevel.init(rawValue:)),
|
||||
includePublicAndOpen || [.internal, .private, .fileprivate].contains(acl) {
|
||||
// Skip declarations marked as @IBOutlet, @IBAction or @objc
|
||||
// since those might not be referenced in code, but only dynamically (e.g. Interface Builder)
|
||||
if let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
|
||||
["@IBOutlet", "@IBAction", "@objc", "@IBInspectable"].contains(where: annotatedDecl.contains) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Classes marked as @UIApplicationMain are used by the operating system as the entry point into the app.
|
||||
if let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
|
||||
annotatedDecl.contains("@UIApplicationMain") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip declarations that override another. This works for both subclass overrides &
|
||||
// protocol extension overrides.
|
||||
if cursorInfo["key.overrides"] != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sometimes default protocol implementations don't have `key.overrides` set but they do have
|
||||
// `key.related_decls`.
|
||||
if cursorInfo["key.related_decls"] != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip CodingKeys as they are used
|
||||
if kind == .enum,
|
||||
cursorInfo.name == "CodingKeys",
|
||||
let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
|
||||
annotatedDecl.contains("usr=\"s:s9CodingKeyP\">CodingKey<") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (usr, Int(offset))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func referencedUSR(cursorInfo: [String: SourceKitRepresentable]) -> String? {
|
||||
if let usr = cursorInfo["key.usr"] as? String,
|
||||
let kind = cursorInfo["key.kind"] as? String,
|
||||
kind.contains("source.lang.swift.ref") {
|
||||
return usr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func testCaseUSR(cursorInfo: [String: SourceKitRepresentable]) -> String? {
|
||||
if let kind = (cursorInfo["key.kind"] as? String).flatMap(SwiftDeclarationKind.init(rawValue:)),
|
||||
kind == .class,
|
||||
let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
|
||||
annotatedDecl.contains("<Type usr=\"c:objc(cs)XCTestCase\">XCTestCase</Type>"),
|
||||
let usr = cursorInfo["key.usr"] as? String {
|
||||
return usr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func aclAtOffset(_ offset: Int64, substructureElement: [String: SourceKitRepresentable]) -> String? {
|
||||
if let nameOffset = substructureElement["key.nameoffset"] as? Int64,
|
||||
nameOffset == offset,
|
||||
let acl = substructureElement["key.accessibility"] as? String {
|
||||
return acl
|
||||
}
|
||||
if let substructure = substructureElement[SwiftDocKey.substructure.rawValue] as? [SourceKitRepresentable] {
|
||||
let nestedSubstructure = substructure.compactMap({ $0 as? [String: SourceKitRepresentable] })
|
||||
for child in nestedSubstructure {
|
||||
if let acl = File.aclAtOffset(offset, substructureElement: child) {
|
||||
return acl
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Skip initializers, deinit, enum cases and subscripts since we can't reliably detect if they're used.
|
||||
private let declarationKindsToSkip: Set<SwiftDeclarationKind> = [
|
||||
.functionConstructor,
|
||||
.functionDestructor,
|
||||
.enumelement,
|
||||
.functionSubscript
|
||||
]
|
||||
|
||||
/// Skip syntax kinds that won't respond to cursor info requests.
|
||||
private let syntaxKindsToSkip: Set<SyntaxKind> = [
|
||||
.attributeBuiltin,
|
||||
.attributeID,
|
||||
.comment,
|
||||
.commentMark,
|
||||
.commentURL,
|
||||
.buildconfigID,
|
||||
.buildconfigKeyword,
|
||||
.docComment,
|
||||
.docCommentField,
|
||||
.keyword,
|
||||
.number,
|
||||
.string,
|
||||
.stringInterpolationAnchor
|
||||
]
|
|
@ -0,0 +1,43 @@
|
|||
private enum ConfigurationKey: String {
|
||||
case severity = "severity"
|
||||
case includePublicAndOpen = "include_public_and_open"
|
||||
}
|
||||
|
||||
public struct UnusedDeclarationConfiguration: RuleConfiguration, Equatable {
|
||||
private(set) var includePublicAndOpen: Bool
|
||||
private(set) var severity: ViolationSeverity
|
||||
|
||||
public var consoleDescription: String {
|
||||
return "\(ConfigurationKey.severity.rawValue): \(severity.rawValue), " +
|
||||
"\(ConfigurationKey.includePublicAndOpen.rawValue): \(includePublicAndOpen)"
|
||||
}
|
||||
|
||||
public init(severity: ViolationSeverity, includePublicAndOpen: Bool) {
|
||||
self.includePublicAndOpen = includePublicAndOpen
|
||||
self.severity = severity
|
||||
}
|
||||
|
||||
public mutating func apply(configuration: Any) throws {
|
||||
guard let configDict = configuration as? [String: Any], !configDict.isEmpty else {
|
||||
throw ConfigurationError.unknownConfiguration
|
||||
}
|
||||
|
||||
for (string, value) in configDict {
|
||||
guard let key = ConfigurationKey(rawValue: string) else {
|
||||
throw ConfigurationError.unknownConfiguration
|
||||
}
|
||||
switch (key, value) {
|
||||
case (.severity, let stringValue as String):
|
||||
if let severityValue = ViolationSeverity(rawValue: stringValue) {
|
||||
severity = severityValue
|
||||
} else {
|
||||
throw ConfigurationError.unknownConfiguration
|
||||
}
|
||||
case (.includePublicAndOpen, let boolValue as Bool):
|
||||
includePublicAndOpen = boolValue
|
||||
default:
|
||||
throw ConfigurationError.unknownConfiguration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,10 @@ struct AnalyzeCommand: CommandProtocol {
|
|||
}
|
||||
|
||||
private func autocorrect(_ options: LintOrAnalyzeOptions) -> Result<(), CommandantError<()>> {
|
||||
return Configuration(options: options).visitLintableFiles(options: options, cache: nil) { linter in
|
||||
let corrections = linter.correct()
|
||||
let storage = RuleStorage()
|
||||
let configuration = Configuration(options: options)
|
||||
return configuration.visitLintableFiles(options: options, cache: nil, storage: storage) { linter in
|
||||
let corrections = linter.correct(using: storage)
|
||||
if !corrections.isEmpty && !options.quiet {
|
||||
let correctionLogs = corrections.map({ $0.consoleDescription })
|
||||
queuedPrint(correctionLogs.joined(separator: "\n"))
|
||||
|
|
|
@ -8,8 +8,9 @@ struct AutoCorrectCommand: CommandProtocol {
|
|||
|
||||
func run(_ options: AutoCorrectOptions) -> Result<(), CommandantError<()>> {
|
||||
let configuration = Configuration(options: options)
|
||||
let visitor = options.visitor(with: configuration)
|
||||
return configuration.visitLintableFiles(with: visitor).flatMap { files in
|
||||
let storage = RuleStorage()
|
||||
let visitor = options.visitor(with: configuration, storage: storage)
|
||||
return configuration.visitLintableFiles(with: visitor, storage: storage).flatMap { files in
|
||||
if !options.quiet {
|
||||
let pluralSuffix = { (collection: [Any]) -> String in
|
||||
return collection.count != 1 ? "s" : ""
|
||||
|
@ -63,7 +64,7 @@ struct AutoCorrectOptions: OptionsProtocol {
|
|||
<*> mode <| pathsArgument(action: "correct")
|
||||
}
|
||||
|
||||
fileprivate func visitor(with configuration: Configuration) -> LintableFilesVisitor {
|
||||
fileprivate func visitor(with configuration: Configuration, storage: RuleStorage) -> LintableFilesVisitor {
|
||||
let cache = ignoreCache ? nil : LinterCache(configuration: configuration)
|
||||
return LintableFilesVisitor(paths: paths, action: "Correcting", useSTDIN: false, quiet: quiet,
|
||||
useScriptInputFiles: useScriptInputFiles, forceExclude: forceExclude, cache: cache,
|
||||
|
@ -76,7 +77,7 @@ struct AutoCorrectOptions: OptionsProtocol {
|
|||
linter.format(useTabs: false, indentWidth: count)
|
||||
}
|
||||
}
|
||||
let corrections = linter.correct()
|
||||
let corrections = linter.correct(using: storage)
|
||||
if !corrections.isEmpty && !self.quiet {
|
||||
let correctionLogs = corrections.map({ $0.consoleDescription })
|
||||
queuedPrint(correctionLogs.joined(separator: "\n"))
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../../SwiftLintFramework/Extensions/Array+SwiftLint.swift
|
|
@ -45,14 +45,18 @@ private func scriptInputFiles() -> Result<[File], CommandantError<()>> {
|
|||
}
|
||||
|
||||
#if os(Linux)
|
||||
private func autoreleasepool(block: () -> Void) { block() }
|
||||
private func autoreleasepool<T>(block: () -> T) -> T { return block() }
|
||||
#endif
|
||||
|
||||
extension Configuration {
|
||||
func visitLintableFiles(with visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
|
||||
return getFiles(with: visitor)
|
||||
.flatMap { groupFiles($0, visitor: visitor) }
|
||||
.flatMap { visit(filesPerConfiguration: $0, visitor: visitor) }
|
||||
func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage)
|
||||
-> Result<[File], CommandantError<()>> {
|
||||
return getFiles(with: visitor)
|
||||
.flatMap { groupFiles($0, visitor: visitor) }
|
||||
.map { linters(for: $0, visitor: visitor) }
|
||||
.map { ($0, $0.duplicateFileNames) }
|
||||
.map { collect(linters: $0.0, visitor: visitor, storage: storage, duplicateFileNames: $0.1) }
|
||||
.map { visit(linters: $0.0, visitor: visitor, storage: storage, duplicateFileNames: $0.1) }
|
||||
}
|
||||
|
||||
private func groupFiles(_ files: [File],
|
||||
|
@ -82,29 +86,31 @@ extension Configuration {
|
|||
return .success(groupedFiles)
|
||||
}
|
||||
|
||||
private func outputFilename(for path: NSString, in root: NSString, duplicateFileNames: Set<String>) -> String {
|
||||
let basename = path.lastPathComponent
|
||||
private func outputFilename(for path: String, duplicateFileNames: Set<String>) -> String {
|
||||
guard !duplicateFileNames.isEmpty else {
|
||||
return path
|
||||
}
|
||||
|
||||
let root = self.rootPath ?? FileManager.default.currentDirectoryPath.bridge().standardizingPath
|
||||
let basename = path.bridge().lastPathComponent
|
||||
if !duplicateFileNames.contains(basename) {
|
||||
return basename
|
||||
}
|
||||
|
||||
var pathComponents = path.pathComponents
|
||||
for component in root.pathComponents where pathComponents.first == component {
|
||||
var pathComponents = path.bridge().pathComponents
|
||||
for component in root.bridge().pathComponents where pathComponents.first == component {
|
||||
pathComponents.removeFirst()
|
||||
}
|
||||
|
||||
return pathComponents.joined(separator: "/")
|
||||
}
|
||||
|
||||
private func visit(filesPerConfiguration: [Configuration: [File]],
|
||||
visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
|
||||
var fileIndex = 0
|
||||
private func linters(for filesPerConfiguration: [Configuration: [File]],
|
||||
visitor: LintableFilesVisitor) -> [Linter] {
|
||||
let fileCount = filesPerConfiguration.reduce(0) { $0 + $1.value.count }
|
||||
let root = FileManager.default.currentDirectoryPath.bridge().standardizingPath.bridge()
|
||||
var filesAndConfigurations = [(File, Configuration)]()
|
||||
var allFileNames = Set<String>()
|
||||
var duplicateFileNames = Set<String>()
|
||||
filesAndConfigurations.reserveCapacity(fileCount)
|
||||
|
||||
var linters = [Linter]()
|
||||
linters.reserveCapacity(fileCount)
|
||||
for (config, files) in filesPerConfiguration {
|
||||
let newConfig: Configuration
|
||||
if visitor.cache != nil {
|
||||
|
@ -112,33 +118,79 @@ extension Configuration {
|
|||
} else {
|
||||
newConfig = config
|
||||
}
|
||||
filesAndConfigurations += files.map { ($0, newConfig) }
|
||||
for file in files {
|
||||
if let filename = file.path?.bridge().lastPathComponent {
|
||||
if allFileNames.contains(filename) {
|
||||
duplicateFileNames.insert(filename)
|
||||
}
|
||||
linters += files.map { visitor.linter(forFile: $0, configuration: newConfig) }
|
||||
}
|
||||
return linters
|
||||
}
|
||||
|
||||
allFileNames.insert(filename)
|
||||
private func collect(linters: [Linter],
|
||||
visitor: LintableFilesVisitor,
|
||||
storage: RuleStorage,
|
||||
duplicateFileNames: Set<String>) -> ([CollectedLinter], Set<String>) {
|
||||
var collected = 0
|
||||
let total = linters.filter({ $0.isCollecting }).count
|
||||
let collect = { (linter: Linter) -> CollectedLinter? in
|
||||
let skipFile = visitor.shouldSkipFile(atPath: linter.file.path)
|
||||
if !visitor.quiet, linter.isCollecting, let filePath = linter.file.path {
|
||||
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
|
||||
let increment = {
|
||||
collected += 1
|
||||
if skipFile {
|
||||
queuedPrintError("""
|
||||
Skipping '\(outputFilename)' (\(collected)/\(total)) \
|
||||
because its compiler arguments could not be found
|
||||
""")
|
||||
} else {
|
||||
queuedPrintError("Collecting '\(outputFilename)' (\(collected)/\(total))")
|
||||
}
|
||||
}
|
||||
if visitor.parallel {
|
||||
indexIncrementerQueue.sync(execute: increment)
|
||||
} else {
|
||||
increment()
|
||||
}
|
||||
}
|
||||
}
|
||||
let visit = { (file: File, configuration: Configuration) in
|
||||
let printableFileName = (file.path?.bridge()).map { path in
|
||||
self.outputFilename(for: path, in: root, duplicateFileNames: duplicateFileNames)
|
||||
|
||||
guard !skipFile else {
|
||||
return nil
|
||||
}
|
||||
visitor.visit(file: file, config: configuration, outputFileName: printableFileName,
|
||||
incrementIndex: { fileIndex += 1 }, progress: { "(\(fileIndex)/\(fileCount))" })
|
||||
}
|
||||
if visitor.parallel {
|
||||
DispatchQueue.concurrentPerform(iterations: fileCount) { index in
|
||||
let (file, config) = filesAndConfigurations[index]
|
||||
visit(file, config)
|
||||
|
||||
return autoreleasepool {
|
||||
linter.collect(into: storage)
|
||||
}
|
||||
} else {
|
||||
filesAndConfigurations.forEach(visit)
|
||||
}
|
||||
return .success(filesAndConfigurations.compactMap({ $0.0 }))
|
||||
|
||||
let collectedLinters = visitor.parallel ?
|
||||
linters.parallelCompactMap(transform: collect) :
|
||||
linters.compactMap(collect)
|
||||
return (collectedLinters, duplicateFileNames)
|
||||
}
|
||||
|
||||
private func visit(linters: [CollectedLinter],
|
||||
visitor: LintableFilesVisitor,
|
||||
storage: RuleStorage,
|
||||
duplicateFileNames: Set<String>) -> [File] {
|
||||
var visited = 0
|
||||
let visit = { (linter: CollectedLinter) -> File in
|
||||
if !visitor.quiet, let filePath = linter.file.path {
|
||||
let outputFilename = self.outputFilename(for: filePath, duplicateFileNames: duplicateFileNames)
|
||||
let increment = {
|
||||
visited += 1
|
||||
queuedPrintError("\(visitor.action) '\(outputFilename)' (\(visited)/\(linters.count))")
|
||||
}
|
||||
if visitor.parallel {
|
||||
indexIncrementerQueue.sync(execute: increment)
|
||||
} else {
|
||||
increment()
|
||||
}
|
||||
}
|
||||
|
||||
autoreleasepool {
|
||||
visitor.block(linter)
|
||||
}
|
||||
return linter.file
|
||||
}
|
||||
return visitor.parallel ? linters.parallelMap(transform: visit) : linters.map(visit)
|
||||
}
|
||||
|
||||
fileprivate func getFiles(with visitor: LintableFilesVisitor) -> Result<[File], CommandantError<()>> {
|
||||
|
@ -189,9 +241,11 @@ extension Configuration {
|
|||
cachePath: cachePath)
|
||||
}
|
||||
|
||||
func visitLintableFiles(options: LintOrAnalyzeOptions, cache: LinterCache? = nil,
|
||||
visitorBlock: @escaping (Linter) -> Void) -> Result<[File], CommandantError<()>> {
|
||||
return LintableFilesVisitor.create(options, cache: cache, block: visitorBlock).flatMap(visitLintableFiles)
|
||||
func visitLintableFiles(options: LintOrAnalyzeOptions, cache: LinterCache? = nil, storage: RuleStorage,
|
||||
visitorBlock: @escaping (CollectedLinter) -> Void) -> Result<[File], CommandantError<()>> {
|
||||
return LintableFilesVisitor.create(options, cache: cache, block: visitorBlock).flatMap({ visitor in
|
||||
visitLintableFiles(with: visitor, storage: storage)
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: AutoCorrect Command
|
||||
|
@ -213,34 +267,22 @@ private func isConfigOptional() -> Bool {
|
|||
return !CommandLine.arguments.contains("--config")
|
||||
}
|
||||
|
||||
private extension LintableFilesVisitor {
|
||||
func visit(file: File, config: Configuration, outputFileName: String?, incrementIndex: @escaping () -> Void,
|
||||
progress: @escaping () -> String) {
|
||||
let skipFile = shouldSkipFile(atPath: file.path)
|
||||
if !quiet, let outputFileName = outputFileName {
|
||||
let increment = {
|
||||
incrementIndex()
|
||||
if skipFile {
|
||||
queuedPrintError("""
|
||||
Skipping '\(outputFileName)' \(progress()) because its compiler arguments could not be found
|
||||
""")
|
||||
} else {
|
||||
queuedPrintError("\(self.action) '\(outputFileName)' \(progress())")
|
||||
private struct DuplicateCollector {
|
||||
var all = Set<String>()
|
||||
var duplicates = Set<String>()
|
||||
}
|
||||
|
||||
private extension Collection where Element == Linter {
|
||||
var duplicateFileNames: Set<String> {
|
||||
let collector = reduce(into: DuplicateCollector()) { result, linter in
|
||||
if let filename = linter.file.path?.bridge().lastPathComponent {
|
||||
if result.all.contains(filename) {
|
||||
result.duplicates.insert(filename)
|
||||
}
|
||||
}
|
||||
if parallel {
|
||||
indexIncrementerQueue.sync(execute: increment)
|
||||
} else {
|
||||
increment()
|
||||
|
||||
result.all.insert(filename)
|
||||
}
|
||||
}
|
||||
|
||||
guard !skipFile else {
|
||||
return
|
||||
}
|
||||
|
||||
autoreleasepool {
|
||||
block(linter(forFile: file, configuration: config))
|
||||
}
|
||||
return collector.duplicates
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,15 +23,16 @@ struct LintOrAnalyzeCommand {
|
|||
var fileBenchmark = Benchmark(name: "files")
|
||||
var ruleBenchmark = Benchmark(name: "rules")
|
||||
var violations = [StyleViolation]()
|
||||
let storage = RuleStorage()
|
||||
let configuration = Configuration(options: options)
|
||||
let reporter = reporterFrom(optionsReporter: options.reporter, configuration: configuration)
|
||||
let cache = options.ignoreCache ? nil : LinterCache(configuration: configuration)
|
||||
let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation")
|
||||
return configuration.visitLintableFiles(options: options, cache: cache) { linter in
|
||||
return configuration.visitLintableFiles(options: options, cache: cache, storage: storage) { linter in
|
||||
let currentViolations: [StyleViolation]
|
||||
if options.benchmark {
|
||||
let start = Date()
|
||||
let (violationsBeforeLeniency, currentRuleTimes) = linter.styleViolationsAndRuleTimes
|
||||
let (violationsBeforeLeniency, currentRuleTimes) = linter.styleViolationsAndRuleTimes(using: storage)
|
||||
currentViolations = applyLeniency(options: options, violations: violationsBeforeLeniency)
|
||||
visitorMutationQueue.sync {
|
||||
fileBenchmark.record(file: linter.file, from: start)
|
||||
|
@ -39,7 +40,7 @@ struct LintOrAnalyzeCommand {
|
|||
violations += currentViolations
|
||||
}
|
||||
} else {
|
||||
currentViolations = applyLeniency(options: options, violations: linter.styleViolations)
|
||||
currentViolations = applyLeniency(options: options, violations: linter.styleViolations(using: storage))
|
||||
visitorMutationQueue.sync {
|
||||
violations += currentViolations
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@ struct LintableFilesVisitor {
|
|||
let cache: LinterCache?
|
||||
let parallel: Bool
|
||||
let mode: LintOrAnalyzeModeWithCompilerArguments
|
||||
let block: (Linter) -> Void
|
||||
let block: (CollectedLinter) -> Void
|
||||
|
||||
init(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, useScriptInputFiles: Bool, forceExclude: Bool,
|
||||
cache: LinterCache?, parallel: Bool, block: @escaping (Linter) -> Void) {
|
||||
cache: LinterCache?, parallel: Bool, block: @escaping (CollectedLinter) -> Void) {
|
||||
self.paths = paths
|
||||
self.action = action
|
||||
self.useSTDIN = useSTDIN
|
||||
|
@ -37,7 +37,7 @@ struct LintableFilesVisitor {
|
|||
|
||||
private init(paths: [String], action: String, useSTDIN: Bool, quiet: Bool, useScriptInputFiles: Bool,
|
||||
forceExclude: Bool, cache: LinterCache?, compilerLogContents: String,
|
||||
block: @escaping (Linter) -> Void) {
|
||||
block: @escaping (CollectedLinter) -> Void) {
|
||||
self.paths = paths
|
||||
self.action = action
|
||||
self.useSTDIN = useSTDIN
|
||||
|
@ -56,7 +56,7 @@ struct LintableFilesVisitor {
|
|||
self.block = block
|
||||
}
|
||||
|
||||
static func create(_ options: LintOrAnalyzeOptions, cache: LinterCache?, block: @escaping (Linter) -> Void)
|
||||
static func create(_ options: LintOrAnalyzeOptions, cache: LinterCache?, block: @escaping (CollectedLinter) -> Void)
|
||||
-> Result<LintableFilesVisitor, CommandantError<()>> {
|
||||
let compilerLogContents: String
|
||||
if options.mode == .lint {
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
1F11B3CF1C252F23002E8FA8 /* ClosingBraceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */; };
|
||||
24B4DF0D1D6DFDE90097803B /* RedundantNilCoalescingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */; };
|
||||
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E17F701B1481FF008195BE /* File+Cache.swift */; };
|
||||
260F66A0225C5B6D00407CF5 /* CollectingRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260F669F225C5B6D00407CF5 /* CollectingRuleTests.swift */; };
|
||||
264E080F2248BA2800ADC4C5 /* RuleStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264E080E2248BA2800ADC4C5 /* RuleStorage.swift */; };
|
||||
26CE462D228532CD00264485 /* Array+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CE462C228532CD00264485 /* Array+SwiftLint.swift */; };
|
||||
287F8B642230843000BDC504 /* NSLocalizedStringRequireBundleRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287F8B62223083ED00BDC504 /* NSLocalizedStringRequireBundleRule.swift */; };
|
||||
2882895F222975D00037CF5F /* NSObjectPreferIsEqualRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2882895B22287C9C0037CF5F /* NSObjectPreferIsEqualRule.swift */; };
|
||||
288289602229776C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2882895D22287E2C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift */; };
|
||||
|
@ -174,6 +177,7 @@
|
|||
85DA81321D6B471000951BC4 /* MarkRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856651A61D6B395F005E6B29 /* MarkRule.swift */; };
|
||||
8B01E4FD20A41C8700C9233E /* FunctionParameterCountConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B01E4FB20A4183C00C9233E /* FunctionParameterCountConfiguration.swift */; };
|
||||
8B01E50220A4349100C9233E /* FunctionParameterCountRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B01E4FF20A4340A00C9233E /* FunctionParameterCountRuleTests.swift */; };
|
||||
8F0856EB22DA8508001FF4D4 /* UnusedDeclarationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0856EA22DA8508001FF4D4 /* UnusedDeclarationRule.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 */; };
|
||||
|
@ -186,6 +190,7 @@
|
|||
8FD216CC205584AF008ED13F /* CharacterSet+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD216CB205584AF008ED13F /* CharacterSet+SwiftLint.swift */; };
|
||||
8FDF482C2122476D00521605 /* AnalyzeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FDF482B2122476D00521605 /* AnalyzeCommand.swift */; };
|
||||
8FDF482E21234BFF00521605 /* LintOrAnalyzeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FDF482D21234BFF00521605 /* LintOrAnalyzeCommand.swift */; };
|
||||
8FE3CCBC22DBF8D000B8EA87 /* UnusedDeclarationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE3CCBB22DBF8D000B8EA87 /* UnusedDeclarationConfiguration.swift */; };
|
||||
92CCB2D71E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */; };
|
||||
93E0C3CE1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E0C3CD1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift */; };
|
||||
A1A6F3F21EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A6F3F11EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift */; };
|
||||
|
@ -506,6 +511,9 @@
|
|||
1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosingBraceRule.swift; sourceTree = "<group>"; };
|
||||
24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantNilCoalescingRule.swift; sourceTree = "<group>"; };
|
||||
24E17F701B1481FF008195BE /* File+Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "File+Cache.swift"; sourceTree = "<group>"; };
|
||||
260F669F225C5B6D00407CF5 /* CollectingRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectingRuleTests.swift; sourceTree = "<group>"; };
|
||||
264E080E2248BA2800ADC4C5 /* RuleStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleStorage.swift; sourceTree = "<group>"; };
|
||||
26CE462C228532CD00264485 /* Array+SwiftLint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Array+SwiftLint.swift"; path = "../../SwiftLintFramework/Extensions/Array+SwiftLint.swift"; sourceTree = "<group>"; };
|
||||
287F8B62223083ED00BDC504 /* NSLocalizedStringRequireBundleRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLocalizedStringRequireBundleRule.swift; sourceTree = "<group>"; };
|
||||
2882895B22287C9C0037CF5F /* NSObjectPreferIsEqualRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSObjectPreferIsEqualRule.swift; sourceTree = "<group>"; };
|
||||
2882895D22287E2C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSObjectPreferIsEqualRuleExamples.swift; sourceTree = "<group>"; };
|
||||
|
@ -644,6 +652,7 @@
|
|||
856651A61D6B395F005E6B29 /* MarkRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkRule.swift; sourceTree = "<group>"; };
|
||||
8B01E4FB20A4183C00C9233E /* FunctionParameterCountConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionParameterCountConfiguration.swift; sourceTree = "<group>"; };
|
||||
8B01E4FF20A4340A00C9233E /* FunctionParameterCountRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionParameterCountRuleTests.swift; sourceTree = "<group>"; };
|
||||
8F0856EA22DA8508001FF4D4 /* UnusedDeclarationRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedDeclarationRule.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>"; };
|
||||
|
@ -656,6 +665,7 @@
|
|||
8FD216CB205584AF008ED13F /* CharacterSet+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CharacterSet+SwiftLint.swift"; sourceTree = "<group>"; };
|
||||
8FDF482B2122476D00521605 /* AnalyzeCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyzeCommand.swift; sourceTree = "<group>"; };
|
||||
8FDF482D21234BFF00521605 /* LintOrAnalyzeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LintOrAnalyzeCommand.swift; sourceTree = "<group>"; };
|
||||
8FE3CCBB22DBF8D000B8EA87 /* UnusedDeclarationConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedDeclarationConfiguration.swift; sourceTree = "<group>"; };
|
||||
92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedOptionalBindingRule.swift; sourceTree = "<group>"; };
|
||||
93E0C3CD1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionalReturnsOnNewlineRule.swift; sourceTree = "<group>"; };
|
||||
A1A6F3F11EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectLiteralConfiguration.swift; sourceTree = "<group>"; };
|
||||
|
@ -1017,6 +1027,7 @@
|
|||
BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */,
|
||||
82DB55FF21008F54001C62FF /* TypeContentsOrderConfiguration.swift */,
|
||||
CE8178EB1EAC02CD0063186E /* UnusedOptionalBindingConfiguration.swift */,
|
||||
8FE3CCBB22DBF8D000B8EA87 /* UnusedDeclarationConfiguration.swift */,
|
||||
006204DA1E1E48F900FFFBE1 /* VerticalWhitespaceConfiguration.swift */,
|
||||
);
|
||||
path = RuleConfigurations;
|
||||
|
@ -1121,6 +1132,7 @@
|
|||
7565E5F02262BA0900B0597C /* UnusedCaptureListRule.swift */,
|
||||
D40AD0891E032F9700F48C30 /* UnusedClosureParameterRule.swift */,
|
||||
D4D7320C21E15ED4001C07D9 /* UnusedControlFlowLabelRule.swift */,
|
||||
8F0856EA22DA8508001FF4D4 /* UnusedDeclarationRule.swift */,
|
||||
8F715B82213B528B00427BD9 /* UnusedImportRule.swift */,
|
||||
D4BED5F72278AECC00D86BCE /* UnownedVariableCaptureRule.swift */,
|
||||
8F6B3153213CDCD100858E44 /* UnusedPrivateDeclarationRule.swift */,
|
||||
|
@ -1422,9 +1434,11 @@
|
|||
children = (
|
||||
D4998DE61DF191380006E05D /* AttributesRuleTests.swift */,
|
||||
D4EAB3A320E9948D0051C09A /* AutomaticRuleTests.generated.swift */,
|
||||
260F669F225C5B6D00407CF5 /* CollectingRuleTests.swift */,
|
||||
7578C915214173BE0080FEC9 /* CollectionAlignmentRuleTests.swift */,
|
||||
D43B04651E071ED3004016AF /* ColonRuleTests.swift */,
|
||||
E81ADD731ED6052F000CD451 /* CommandTests.swift */,
|
||||
BCB68282216213130078E4C3 /* CompilerProtocolInitRuleTests.swift */,
|
||||
820F451D21073D7200AA056A /* ConditionalReturnsOnNewlineRuleTests.swift */,
|
||||
D0D1212219E878CC005E4BAA /* Configuration */,
|
||||
C2B3C15F2106F78100088928 /* ConfigurationAliasesTests.swift */,
|
||||
|
@ -1435,12 +1449,12 @@
|
|||
3BB47D861C51DE6E00AE6A10 /* CustomRulesTests.swift */,
|
||||
67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */,
|
||||
67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */,
|
||||
CCD8B87720559C4A00B75847 /* DisableAllTests.swift */,
|
||||
D41985EC21FAD033003BE2B7 /* DeploymentTargetConfigurationTests.swift */,
|
||||
D41985EE21FAD5E8003BE2B7 /* DeploymentTargetRuleTests.swift */,
|
||||
CCD8B87720559C4A00B75847 /* DisableAllTests.swift */,
|
||||
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */,
|
||||
D48B51221F4F5E4B0068AB98 /* DocumentationTests.swift */,
|
||||
D414D6AB21D0B77F00960935 /* DiscouragedObjectLiteralRuleTests.swift */,
|
||||
D48B51221F4F5E4B0068AB98 /* DocumentationTests.swift */,
|
||||
125CE52E20425EFD001635E5 /* ExplicitTypeInterfaceConfigurationTests.swift */,
|
||||
12E3D4DB2042729300B3E30E /* ExplicitTypeInterfaceRuleTests.swift */,
|
||||
02FD8AEE1BFC18D60014BFFB /* ExtendedNSStringTests.swift */,
|
||||
|
@ -1481,9 +1495,9 @@
|
|||
787CDE3A208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift */,
|
||||
E81224991B04F85B001783D2 /* TestHelpers.swift */,
|
||||
D4DB92241E628898005DE9C1 /* TodoRuleTests.swift */,
|
||||
C9802F2E1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift */,
|
||||
D450D1E121F19B1E00E60010 /* TrailingClosureConfigurationTests.swift */,
|
||||
D450D1DE21F19A9400E60010 /* TrailingClosureRuleTests.swift */,
|
||||
C9802F2E1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift */,
|
||||
D4F5851320E99A720085C6D8 /* TrailingWhitespaceTests.swift */,
|
||||
820F451B2107292500AA056A /* TypeContentsOrderRuleTests.swift */,
|
||||
3B20CD0B1EB699C20069EF2E /* TypeNameRuleTests.swift */,
|
||||
|
@ -1493,7 +1507,6 @@
|
|||
627C7A312004F9290053C79D /* XCTSpecificMatcherRuleTests.swift */,
|
||||
3B30C4A01C3785B300E04027 /* YamlParserTests.swift */,
|
||||
3B12C9C21C320A53000B423F /* YamlSwiftLintTests.swift */,
|
||||
BCB68282216213130078E4C3 /* CompilerProtocolInitRuleTests.swift */,
|
||||
);
|
||||
name = SwiftLintFrameworkTests;
|
||||
path = Tests/SwiftLintFrameworkTests;
|
||||
|
@ -1598,6 +1611,7 @@
|
|||
D4C27BFD1E12D53F00DF713E /* Version.swift */,
|
||||
E88DEA701B09847500A66CB0 /* ViolationSeverity.swift */,
|
||||
3BD9CD3C1C37175B009A5D25 /* YamlParser.swift */,
|
||||
264E080E2248BA2800ADC4C5 /* RuleStorage.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1635,6 +1649,7 @@
|
|||
E8B0677F1C13E48100E9E13F /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
26CE462C228532CD00264485 /* Array+SwiftLint.swift */,
|
||||
E8B067801C13E49600E9E13F /* Configuration+CommandLine.swift */,
|
||||
E86E2B2D1E17443B001E823C /* Reporter+CommandLine.swift */,
|
||||
);
|
||||
|
@ -1951,6 +1966,7 @@
|
|||
29FC197A21382C07006D208C /* DuplicateImportsRuleExamples.swift in Sources */,
|
||||
83D71E281B131ECE000395DE /* RuleDescription.swift in Sources */,
|
||||
D4470D571EB69225008A1B2E /* ImplicitReturnRule.swift in Sources */,
|
||||
8FE3CCBC22DBF8D000B8EA87 /* UnusedDeclarationConfiguration.swift in Sources */,
|
||||
3B12C9C51C322032000B423F /* MasterRuleList.swift in Sources */,
|
||||
E812249C1B04FADC001783D2 /* Linter.swift in Sources */,
|
||||
1F11B3CF1C252F23002E8FA8 /* ClosingBraceRule.swift in Sources */,
|
||||
|
@ -2125,6 +2141,8 @@
|
|||
E88198421BEA929F00333A11 /* NestingRule.swift in Sources */,
|
||||
D46A317F1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift in Sources */,
|
||||
D4470D591EB6B4D1008A1B2E /* EmptyEnumArgumentsRule.swift in Sources */,
|
||||
8F0856EB22DA8508001FF4D4 /* UnusedDeclarationRule.swift in Sources */,
|
||||
264E080F2248BA2800ADC4C5 /* RuleStorage.swift in Sources */,
|
||||
627BC48D1F9405160004A6C2 /* QuickDiscouragedFocusedTestRule.swift in Sources */,
|
||||
3BB47D851C51D80000AE6A10 /* NSRegularExpression+SwiftLint.swift in Sources */,
|
||||
E881985B1BEA974E00333A11 /* StatementPositionRule.swift in Sources */,
|
||||
|
@ -2284,6 +2302,7 @@
|
|||
C9802F2F1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift in Sources */,
|
||||
3B63D46F1E1F09DF0057BE35 /* LineLengthRuleTests.swift in Sources */,
|
||||
627C7A322004F9290053C79D /* XCTSpecificMatcherRuleTests.swift in Sources */,
|
||||
260F66A0225C5B6D00407CF5 /* CollectingRuleTests.swift in Sources */,
|
||||
3BCC04D41C502BAB006073C3 /* RuleConfigurationTests.swift in Sources */,
|
||||
E809EDA31B8A73FB00399043 /* ConfigurationTests.swift in Sources */,
|
||||
8F2CC1CD20A6A189006ED34F /* FileNameRuleTests.swift in Sources */,
|
||||
|
@ -2301,6 +2320,7 @@
|
|||
E802ED001C56A56000A35AE1 /* Benchmark.swift in Sources */,
|
||||
E83A0B351A5D382B0041A60A /* VersionCommand.swift in Sources */,
|
||||
E81FB3E41C6D507B00DC988F /* CommonOptions.swift in Sources */,
|
||||
26CE462D228532CD00264485 /* Array+SwiftLint.swift in Sources */,
|
||||
E861519B1B0573B900C54AC0 /* LintCommand.swift in Sources */,
|
||||
D0E7B65619E9C76900EDBA4D /* main.swift in Sources */,
|
||||
83894F221B0C928A006214E1 /* RulesCommand.swift in Sources */,
|
||||
|
|
|
@ -69,7 +69,8 @@
|
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
useCustomWorkingDirectory = "YES"
|
||||
customWorkingDirectory = "~/Projects/Lyft-iOS"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "NO"
|
||||
debugXPCServices = "NO"
|
||||
|
@ -88,9 +89,13 @@
|
|||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "lint --no-cache"
|
||||
argument = "analyze --quiet --strict --compiler-log-path xcodebuild.log"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "lint --no-cache"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
|
|
|
@ -69,6 +69,15 @@ extension ClosureSpacingRuleTests {
|
|||
]
|
||||
}
|
||||
|
||||
extension CollectingRuleTests {
|
||||
static var allTests: [(String, (CollectingRuleTests) -> () throws -> Void)] = [
|
||||
("testCollectsIntoStorage", testCollectsIntoStorage),
|
||||
("testCollectsAllFiles", testCollectsAllFiles),
|
||||
("testCollectsAnalyzerFiles", testCollectsAnalyzerFiles),
|
||||
("testCorrects", testCorrects)
|
||||
]
|
||||
}
|
||||
|
||||
extension CollectionAlignmentRuleTests {
|
||||
static var allTests: [(String, (CollectionAlignmentRuleTests) -> () throws -> Void)] = [
|
||||
("testWithDefaultConfiguration", testWithDefaultConfiguration),
|
||||
|
@ -1381,6 +1390,12 @@ extension UnusedControlFlowLabelRuleTests {
|
|||
]
|
||||
}
|
||||
|
||||
extension UnusedDeclarationRuleTests {
|
||||
static var allTests: [(String, (UnusedDeclarationRuleTests) -> () throws -> Void)] = [
|
||||
("testWithDefaultConfiguration", testWithDefaultConfiguration)
|
||||
]
|
||||
}
|
||||
|
||||
extension UnusedEnumeratedRuleTests {
|
||||
static var allTests: [(String, (UnusedEnumeratedRuleTests) -> () throws -> Void)] = [
|
||||
("testWithDefaultConfiguration", testWithDefaultConfiguration)
|
||||
|
@ -1532,6 +1547,7 @@ XCTMain([
|
|||
testCase(ClosureEndIndentationRuleTests.allTests),
|
||||
testCase(ClosureParameterPositionRuleTests.allTests),
|
||||
testCase(ClosureSpacingRuleTests.allTests),
|
||||
testCase(CollectingRuleTests.allTests),
|
||||
testCase(CollectionAlignmentRuleTests.allTests),
|
||||
testCase(ColonRuleTests.allTests),
|
||||
testCase(CommaRuleTests.allTests),
|
||||
|
@ -1700,6 +1716,7 @@ XCTMain([
|
|||
testCase(UnusedCaptureListRuleTests.allTests),
|
||||
testCase(UnusedClosureParameterRuleTests.allTests),
|
||||
testCase(UnusedControlFlowLabelRuleTests.allTests),
|
||||
testCase(UnusedDeclarationRuleTests.allTests),
|
||||
testCase(UnusedEnumeratedRuleTests.allTests),
|
||||
testCase(UnusedImportRuleTests.allTests),
|
||||
testCase(UnusedOptionalBindingRuleTests.allTests),
|
||||
|
|
|
@ -714,6 +714,12 @@ class UnusedControlFlowLabelRuleTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
class UnusedDeclarationRuleTests: XCTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule(UnusedDeclarationRule.description)
|
||||
}
|
||||
}
|
||||
|
||||
class UnusedEnumeratedRuleTests: XCTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule(UnusedEnumeratedRule.description)
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import SourceKittenFramework
|
||||
@testable import SwiftLintFramework
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable nesting
|
||||
|
||||
class CollectingRuleTests: XCTestCase {
|
||||
func testCollectsIntoStorage() {
|
||||
struct Spec: MockCollectingRule {
|
||||
func collectInfo(for file: File) -> Int {
|
||||
return 42
|
||||
}
|
||||
func validate(file: File, collectedInfo: [File: Int]) -> [StyleViolation] {
|
||||
XCTAssertEqual(collectedInfo[file], 42)
|
||||
return [StyleViolation(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 0))]
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertFalse(violations("", config: Spec.configuration!).isEmpty)
|
||||
}
|
||||
|
||||
func testCollectsAllFiles() {
|
||||
struct Spec: MockCollectingRule {
|
||||
func collectInfo(for file: File) -> String {
|
||||
return file.contents
|
||||
}
|
||||
func validate(file: File, collectedInfo: [File: String]) -> [StyleViolation] {
|
||||
let values = collectedInfo.values
|
||||
XCTAssertTrue(values.contains("foo"))
|
||||
XCTAssertTrue(values.contains("bar"))
|
||||
XCTAssertTrue(values.contains("baz"))
|
||||
return [StyleViolation(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 0))]
|
||||
}
|
||||
}
|
||||
|
||||
let inputs = ["foo", "bar", "baz"]
|
||||
XCTAssertEqual(inputs.violations(config: Spec.configuration!).count, inputs.count)
|
||||
}
|
||||
|
||||
func testCollectsAnalyzerFiles() {
|
||||
struct Spec: MockCollectingRule & AnalyzerRule {
|
||||
func collectInfo(for file: File, compilerArguments: [String]) -> [String] {
|
||||
return compilerArguments
|
||||
}
|
||||
func validate(file: File, collectedInfo: [File: [String]], compilerArguments: [String])
|
||||
-> [StyleViolation] {
|
||||
XCTAssertEqual(collectedInfo[file], compilerArguments)
|
||||
return [StyleViolation(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 0))]
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertFalse(violations("", config: Spec.configuration!, requiresFileOnDisk: true).isEmpty)
|
||||
}
|
||||
|
||||
func testCorrects() {
|
||||
struct Spec: MockCollectingRule & CollectingCorrectableRule {
|
||||
func collectInfo(for file: File) -> String {
|
||||
return file.contents
|
||||
}
|
||||
|
||||
func validate(file: File, collectedInfo: [File: String]) -> [StyleViolation] {
|
||||
if collectedInfo[file] == "baz" {
|
||||
return [StyleViolation(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 2))]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func correct(file: File, collectedInfo: [File: String]) -> [Correction] {
|
||||
if collectedInfo[file] == "baz" {
|
||||
return [Correction(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 2))]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AnalyzerSpec: MockCollectingRule & AnalyzerRule & CollectingCorrectableRule {
|
||||
func collectInfo(for file: File, compilerArguments: [String]) -> String {
|
||||
return file.contents
|
||||
}
|
||||
|
||||
func validate(file: File, collectedInfo: [File: String], compilerArguments: [String])
|
||||
-> [StyleViolation] {
|
||||
if collectedInfo[file] == "baz" {
|
||||
return [StyleViolation(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 2))]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func correct(file: File, collectedInfo: [File: String], compilerArguments: [String]) -> [Correction] {
|
||||
if collectedInfo[file] == "baz" {
|
||||
return [Correction(ruleDescription: Spec.description,
|
||||
location: Location(file: file, byteOffset: 2))]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let inputs = ["foo", "baz"]
|
||||
XCTAssertEqual(inputs.corrections(config: Spec.configuration!).count, 1)
|
||||
XCTAssertEqual(inputs.corrections(config: AnalyzerSpec.configuration!, requiresFileOnDisk: true).count, 1)
|
||||
}
|
||||
}
|
||||
|
||||
private protocol MockCollectingRule: CollectingRule {}
|
||||
extension MockCollectingRule {
|
||||
var configurationDescription: String { return "N/A" }
|
||||
static var description: RuleDescription {
|
||||
return RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint)
|
||||
}
|
||||
static var configuration: Configuration? {
|
||||
return Configuration(rulesMode: .whitelisted([description.identifier]), ruleList: RuleList(rules: self))
|
||||
}
|
||||
|
||||
init(configuration: Any) throws { self.init() }
|
||||
}
|
|
@ -7,10 +7,6 @@ private func funcWithParameters(_ parameters: String, violates: Bool = false) ->
|
|||
return "func \(marker)abc(\(parameters)) {}\n"
|
||||
}
|
||||
|
||||
private func violatingFuncWithParameters(_ parameters: String) -> String {
|
||||
return funcWithParameters(parameters, violates: true)
|
||||
}
|
||||
|
||||
class FunctionParameterCountRuleTests: XCTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule(FunctionParameterCountRule.description)
|
||||
|
@ -48,10 +44,4 @@ class FunctionParameterCountRuleTests: XCTestCase {
|
|||
|
||||
verifyRule(description, ruleConfiguration: ["ignores_default_parameters": false])
|
||||
}
|
||||
|
||||
private func violations(_ string: String) -> [StyleViolation] {
|
||||
let config = makeConfig(nil, FunctionParameterCountRule.description.identifier)!
|
||||
|
||||
return SwiftLintFrameworkTests.violations(string, config: config)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,9 @@ class IntegrationTests: XCTestCase {
|
|||
let swiftFiles = config.lintableFiles(inPath: "", forceExclude: false)
|
||||
XCTAssert(swiftFiles.contains(where: { #file == $0.path }), "current file should be included")
|
||||
|
||||
let storage = RuleStorage()
|
||||
let violations = swiftFiles.parallelFlatMap {
|
||||
Linter(file: $0, configuration: config).styleViolations
|
||||
Linter(file: $0, configuration: config).collect(into: storage).styleViolations(using: storage)
|
||||
}
|
||||
violations.forEach { violation in
|
||||
violation.location.file!.withStaticString {
|
||||
|
@ -30,7 +31,10 @@ class IntegrationTests: XCTestCase {
|
|||
|
||||
func testSwiftLintAutoCorrects() {
|
||||
let swiftFiles = config.lintableFiles(inPath: "", forceExclude: false)
|
||||
let corrections = swiftFiles.parallelFlatMap { Linter(file: $0, configuration: config).correct() }
|
||||
let storage = RuleStorage()
|
||||
let corrections = swiftFiles.parallelFlatMap {
|
||||
Linter(file: $0, configuration: config).collect(into: storage).correct(using: storage)
|
||||
}
|
||||
for correction in corrections {
|
||||
correction.location.file!.withStaticString {
|
||||
XCTFail(correction.ruleDescription.description,
|
||||
|
|
|
@ -65,7 +65,8 @@ class SourceKitCrashTests: XCTestCase {
|
|||
XCTFail("If this called, rule's SourceKitFreeRule is not properly configured")
|
||||
}
|
||||
let configuration = Configuration(rulesMode: .whitelisted(allRuleIdentifiers))!
|
||||
_ = Linter(file: file, configuration: configuration).styleViolations
|
||||
let storage = RuleStorage()
|
||||
_ = Linter(file: file, configuration: configuration).collect(into: storage).styleViolations(using: storage)
|
||||
file.sourcekitdFailed = false
|
||||
file.assertHandler = nil
|
||||
}
|
||||
|
|
|
@ -33,17 +33,78 @@ func violations(_ string: String, config: Configuration = Configuration()!,
|
|||
let stringStrippingMarkers = string.replacingOccurrences(of: violationMarker, with: "")
|
||||
guard requiresFileOnDisk else {
|
||||
let file = File(contents: stringStrippingMarkers)
|
||||
let linter = Linter(file: file, configuration: config)
|
||||
return linter.styleViolations
|
||||
let storage = RuleStorage()
|
||||
let linter = Linter(file: file, configuration: config).collect(into: storage)
|
||||
return linter.styleViolations(using: storage)
|
||||
}
|
||||
|
||||
let file = File.temporary(withContents: stringStrippingMarkers)
|
||||
let linter = Linter(file: file, configuration: config, compilerArguments: file.makeCompilerArguments())
|
||||
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)
|
||||
let storage = RuleStorage()
|
||||
let collecter = Linter(file: file, configuration: config, compilerArguments: file.makeCompilerArguments())
|
||||
let linter = collecter.collect(into: storage)
|
||||
return linter.styleViolations(using: storage).withoutFiles()
|
||||
}
|
||||
|
||||
extension Collection where Element == String {
|
||||
func violations(config: Configuration = Configuration()!, requiresFileOnDisk: Bool = false)
|
||||
-> [StyleViolation] {
|
||||
let makeFile = requiresFileOnDisk ? File.temporary : File.init(contents:)
|
||||
return map(makeFile).violations(config: config, requiresFileOnDisk: requiresFileOnDisk)
|
||||
}
|
||||
|
||||
func corrections(config: Configuration = Configuration()!, requiresFileOnDisk: Bool = false) -> [Correction] {
|
||||
let makeFile = requiresFileOnDisk ? File.temporary : File.init(contents:)
|
||||
return map(makeFile).corrections(config: config, requiresFileOnDisk: requiresFileOnDisk)
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where Element: File {
|
||||
func violations(config: Configuration = Configuration()!, requiresFileOnDisk: Bool = false)
|
||||
-> [StyleViolation] {
|
||||
let storage = RuleStorage()
|
||||
let violations = map({ file in
|
||||
Linter(file: file, configuration: config,
|
||||
compilerArguments: requiresFileOnDisk ? file.makeCompilerArguments() : [])
|
||||
}).map({ linter in
|
||||
linter.collect(into: storage)
|
||||
}).flatMap({ linter in
|
||||
linter.styleViolations(using: storage)
|
||||
})
|
||||
return requiresFileOnDisk ? violations.withoutFiles() : violations
|
||||
}
|
||||
|
||||
func corrections(config: Configuration = Configuration()!, requiresFileOnDisk: Bool = false) -> [Correction] {
|
||||
let storage = RuleStorage()
|
||||
let corrections = map({ file in
|
||||
Linter(file: file, configuration: config,
|
||||
compilerArguments: requiresFileOnDisk ? file.makeCompilerArguments() : [])
|
||||
}).map({ linter in
|
||||
linter.collect(into: storage)
|
||||
}).flatMap({ linter in
|
||||
linter.correct(using: storage)
|
||||
})
|
||||
return requiresFileOnDisk ? corrections.withoutFiles() : corrections
|
||||
}
|
||||
}
|
||||
|
||||
private extension Collection where Element == StyleViolation {
|
||||
func withoutFiles() -> [StyleViolation] {
|
||||
return 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 extension Collection where Element == Correction {
|
||||
func withoutFiles() -> [Correction] {
|
||||
return map { correction in
|
||||
let locationWithoutFile = Location(file: nil, line: correction.location.line,
|
||||
character: correction.location.character)
|
||||
return Correction(ruleDescription: correction.ruleDescription, location: locationWithoutFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,8 +159,10 @@ private extension Configuration {
|
|||
let expectedLocations = markerOffsets.map { Location(file: file, characterOffset: $0) }
|
||||
let includeCompilerArguments = self.rules.contains(where: { $0 is AnalyzerRule })
|
||||
let compilerArguments = includeCompilerArguments ? file.makeCompilerArguments() : []
|
||||
let linter = Linter(file: file, configuration: self, compilerArguments: compilerArguments)
|
||||
let corrections = linter.correct().sorted { $0.location < $1.location }
|
||||
let storage = RuleStorage()
|
||||
let collecter = Linter(file: file, configuration: self, compilerArguments: compilerArguments)
|
||||
let linter = collecter.collect(into: storage)
|
||||
let corrections = linter.correct(using: storage).sorted { $0.location < $1.location }
|
||||
if expectedLocations.isEmpty {
|
||||
XCTAssertEqual(corrections.count, before != expected ? 1 : 0)
|
||||
} else {
|
||||
|
|
|
@ -154,7 +154,7 @@ def generate_reports(branch)
|
|||
command = '../builds/.build/release/swiftlint lint --no-cache --enable-all-rules --reporter xcode'
|
||||
File.open("../#{branch}_reports/#{repo}.txt", 'w') do |file|
|
||||
puts "\n#{command}" if @options[:verbose]
|
||||
Open3.popen3(command) do |_, stdout, _, wait_thr|
|
||||
Open3.popen2(command) do |_, stdout, wait_thr|
|
||||
while line = stdout.gets
|
||||
file.puts line
|
||||
end
|
||||
|
@ -170,7 +170,7 @@ def generate_reports(branch)
|
|||
print "..#{i}"
|
||||
start = Time.now
|
||||
puts command if @options[:verbose]
|
||||
Open3.popen3(command) { |_, stdout, _, _| stdout.read }
|
||||
Open3.popen2(command) { |_, stdout, _| stdout.read }
|
||||
durations << Time.now - start
|
||||
end
|
||||
puts ''
|
||||
|
|
Loading…
Reference in New Issue