mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
b83e0991b9
The MIT license doesn't require that all files be prepended with this licensing or copyright information. Realm confirmed that they're ok with this change. This will enable some companies to contribute to SwiftLint and the date & authorship information will remain accessible via git source control.
370 lines
18 KiB
Swift
370 lines
18 KiB
Swift
import Foundation
|
|
@testable import SwiftLintFramework
|
|
import XCTest
|
|
|
|
private struct CacheTestHelper {
|
|
fileprivate let configuration: Configuration
|
|
|
|
private let ruleList: RuleList
|
|
private let ruleDescription: RuleDescription
|
|
private let cache: LinterCache
|
|
|
|
private var fileManager: TestFileManager {
|
|
// swiftlint:disable:next force_cast
|
|
return cache.fileManager as! TestFileManager
|
|
}
|
|
|
|
fileprivate init(dict: [String: Any], cache: LinterCache) {
|
|
ruleList = RuleList(rules: RuleWithLevelsMock.self)
|
|
ruleDescription = ruleList.list.values.first!.description
|
|
configuration = Configuration(dict: dict, ruleList: ruleList)!
|
|
self.cache = cache
|
|
}
|
|
|
|
fileprivate func makeViolations(file: String) -> [StyleViolation] {
|
|
touch(file: file)
|
|
return [
|
|
StyleViolation(ruleDescription: ruleDescription,
|
|
severity: .warning,
|
|
location: Location(file: file, line: 10, character: 2),
|
|
reason: "Something is not right."),
|
|
StyleViolation(ruleDescription: ruleDescription,
|
|
severity: .error,
|
|
location: Location(file: file, line: 5, character: nil),
|
|
reason: "Something is wrong.")
|
|
]
|
|
}
|
|
|
|
fileprivate func makeConfig(dict: [String: Any]) -> Configuration {
|
|
return Configuration(dict: dict, ruleList: ruleList)!
|
|
}
|
|
|
|
fileprivate func touch(file: String) {
|
|
fileManager.stubbedModificationDateByPath[file] = Date()
|
|
}
|
|
|
|
fileprivate func remove(file: String) {
|
|
fileManager.stubbedModificationDateByPath[file] = nil
|
|
}
|
|
|
|
fileprivate func fileCount() -> Int {
|
|
return fileManager.stubbedModificationDateByPath.count
|
|
}
|
|
}
|
|
|
|
private class TestFileManager: LintableFileManager {
|
|
fileprivate func filesToLint(inPath: String, rootDirectory: String? = nil) -> [String] {
|
|
return []
|
|
}
|
|
|
|
fileprivate var stubbedModificationDateByPath = [String: Date]()
|
|
|
|
fileprivate func modificationDate(forFileAtPath path: String) -> Date? {
|
|
return stubbedModificationDateByPath[path]
|
|
}
|
|
}
|
|
|
|
class LinterCacheTests: XCTestCase {
|
|
|
|
// MARK: Test Helpers
|
|
|
|
private var cache = LinterCache(fileManager: TestFileManager())
|
|
|
|
private func makeCacheTestHelper(dict: [String: Any]) -> CacheTestHelper {
|
|
return CacheTestHelper(dict: dict, cache: cache)
|
|
}
|
|
|
|
private func cacheAndValidate(violations: [StyleViolation], forFile: String, configuration: Configuration,
|
|
file: StaticString = #file, line: UInt = #line) {
|
|
cache.cache(violations: violations, forFile: forFile, configuration: configuration)
|
|
cache = cache.flushed()
|
|
XCTAssertEqual(cache.violations(forFile: forFile, configuration: configuration)!,
|
|
violations, file: file, line: line)
|
|
}
|
|
|
|
private func cacheAndValidateNoViolationsTwoFiles(configuration: Configuration,
|
|
file: StaticString = #file, line: UInt = #line) {
|
|
let (file1, file2) = ("file1.swift", "file2.swift")
|
|
// swiftlint:disable:next force_cast
|
|
let fileManager = cache.fileManager as! TestFileManager
|
|
fileManager.stubbedModificationDateByPath = [file1: Date(), file2: Date()]
|
|
|
|
cacheAndValidate(violations: [], forFile: file1, configuration: configuration, file: file, line: line)
|
|
cacheAndValidate(violations: [], forFile: file2, configuration: configuration, file: file, line: line)
|
|
}
|
|
|
|
private func validateNewConfigDoesntHitCache(dict: [String: Any], initialConfig: Configuration,
|
|
file: StaticString = #file, line: UInt = #line) {
|
|
let newConfig = Configuration(dict: dict)!
|
|
let (file1, file2) = ("file1.swift", "file2.swift")
|
|
|
|
XCTAssertNil(cache.violations(forFile: file1, configuration: newConfig), file: file, line: line)
|
|
XCTAssertNil(cache.violations(forFile: file2, configuration: newConfig), file: file, line: line)
|
|
|
|
XCTAssertEqual(cache.violations(forFile: file1, configuration: initialConfig)!, [], file: file, line: line)
|
|
XCTAssertEqual(cache.violations(forFile: file2, configuration: initialConfig)!, [], file: file, line: line)
|
|
}
|
|
|
|
// MARK: Cache Initialization
|
|
|
|
func testInitThrowsWhenUsingInvalidCacheFormat() {
|
|
let cache = [["version": "0.1.0"]]
|
|
checkError(LinterCacheError.invalidFormat) {
|
|
_ = try LinterCache(cache: cache)
|
|
}
|
|
}
|
|
|
|
func testSaveThrowsWithNoLocation() throws {
|
|
let cache = try LinterCache(cache: [:])
|
|
checkError(LinterCacheError.noLocation) {
|
|
try cache.save()
|
|
}
|
|
}
|
|
|
|
func testInitSucceeds() {
|
|
XCTAssertNotNil(try? LinterCache(cache: [:]))
|
|
}
|
|
|
|
// MARK: Cache Reuse
|
|
|
|
// Two subsequent lints with no changes reuses cache
|
|
func testUnchangedFilesReusesCache() {
|
|
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"]])
|
|
let file = "foo.swift"
|
|
let violations = helper.makeViolations(file: file)
|
|
|
|
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
|
|
helper.touch(file: file)
|
|
|
|
XCTAssertNil(cache.violations(forFile: file, configuration: helper.configuration))
|
|
}
|
|
|
|
func testConfigFileReorderedReusesCache() {
|
|
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "disabled_rules": []])
|
|
let file = "foo.swift"
|
|
let violations = helper.makeViolations(file: file)
|
|
|
|
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
|
|
let configuration2 = helper.makeConfig(dict: ["disabled_rules": [], "whitelist_rules": ["mock"]])
|
|
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration2)!, violations)
|
|
}
|
|
|
|
func testConfigFileWhitespaceAndCommentsChangedOrAddedOrRemovedReusesCache() throws {
|
|
let helper = makeCacheTestHelper(dict: try YamlParser.parse("whitelist_rules:\n - mock"))
|
|
let file = "foo.swift"
|
|
let violations = helper.makeViolations(file: file)
|
|
|
|
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
|
|
let configuration2 = helper.makeConfig(dict: ["disabled_rules": [], "whitelist_rules": ["mock"]])
|
|
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration2)!, violations)
|
|
let configYamlWithComment = try YamlParser.parse("# comment1\nwhitelist_rules:\n - mock # comment2")
|
|
let configuration3 = helper.makeConfig(dict: configYamlWithComment)
|
|
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration3)!, violations)
|
|
XCTAssertEqual(cache.violations(forFile: file, configuration: helper.configuration)!, violations)
|
|
}
|
|
|
|
func testConfigFileUnrelatedKeysChangedOrAddedOrRemovedReusesCache() {
|
|
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "reporter": "json"])
|
|
let file = "foo.swift"
|
|
let violations = helper.makeViolations(file: file)
|
|
|
|
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
|
|
let configuration2 = helper.makeConfig(dict: ["whitelist_rules": ["mock"], "reporter": "xcode"])
|
|
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration2)!, violations)
|
|
let configuration3 = helper.makeConfig(dict: ["whitelist_rules": ["mock"]])
|
|
XCTAssertEqual(cache.violations(forFile: file, configuration: configuration3)!, violations)
|
|
}
|
|
|
|
// MARK: Sing-File Cache Invalidation
|
|
|
|
// Two subsequent lints with a file touch in between causes just that one
|
|
// file to be re-linted, with the cache used for all other files
|
|
func testChangedFileCausesJustThatFileToBeLintWithCacheUsedForAllOthers() {
|
|
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "reporter": "json"])
|
|
let (file1, file2) = ("file1.swift", "file2.swift")
|
|
let violations1 = helper.makeViolations(file: file1)
|
|
let violations2 = helper.makeViolations(file: file2)
|
|
|
|
cacheAndValidate(violations: violations1, forFile: file1, configuration: helper.configuration)
|
|
cacheAndValidate(violations: violations2, forFile: file2, configuration: helper.configuration)
|
|
helper.touch(file: file2)
|
|
XCTAssertEqual(cache.violations(forFile: file1, configuration: helper.configuration)!, violations1)
|
|
XCTAssertNil(cache.violations(forFile: file2, configuration: helper.configuration))
|
|
}
|
|
|
|
func testFileRemovedPreservesThatFileInTheCacheAndDoesntCauseAnyOtherFilesToBeLinted() {
|
|
let helper = makeCacheTestHelper(dict: ["whitelist_rules": ["mock"], "reporter": "json"])
|
|
let (file1, file2) = ("file1.swift", "file2.swift")
|
|
let violations1 = helper.makeViolations(file: file1)
|
|
let violations2 = helper.makeViolations(file: file2)
|
|
|
|
cacheAndValidate(violations: violations1, forFile: file1, configuration: helper.configuration)
|
|
cacheAndValidate(violations: violations2, forFile: file2, configuration: helper.configuration)
|
|
XCTAssertEqual(helper.fileCount(), 2)
|
|
helper.remove(file: file2)
|
|
XCTAssertEqual(cache.violations(forFile: file1, configuration: helper.configuration)!, violations1)
|
|
XCTAssertEqual(helper.fileCount(), 1)
|
|
}
|
|
|
|
// MARK: All-File Cache Invalidation
|
|
|
|
func testCustomRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
|
|
let initialConfig = Configuration(
|
|
dict: [
|
|
"whitelist_rules": ["custom_rules", "rule1"],
|
|
"custom_rules": ["rule1": ["regex": "([n,N]inja)"]]
|
|
],
|
|
ruleList: RuleList(rules: CustomRules.self)
|
|
)!
|
|
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
|
|
|
|
// Change
|
|
validateNewConfigDoesntHitCache(
|
|
dict: [
|
|
"whitelist_rules": ["custom_rules", "rule1"],
|
|
"custom_rules": ["rule1": ["regex": "([n,N]injas)"]]
|
|
],
|
|
initialConfig: initialConfig
|
|
)
|
|
|
|
// Addition
|
|
validateNewConfigDoesntHitCache(
|
|
dict: [
|
|
"whitelist_rules": ["custom_rules", "rule1"],
|
|
"custom_rules": ["rule1": ["regex": "([n,N]injas)"], "rule2": ["regex": "([k,K]ittens)"]]
|
|
],
|
|
initialConfig: initialConfig
|
|
)
|
|
|
|
// Removal
|
|
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": ["custom_rules"]], initialConfig: initialConfig)
|
|
}
|
|
|
|
func testDisabledRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
|
|
let initialConfig = Configuration(dict: ["disabled_rules": ["nesting"]])!
|
|
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
|
|
|
|
// Change
|
|
validateNewConfigDoesntHitCache(dict: ["disabled_rules": ["todo"]], initialConfig: initialConfig)
|
|
// Addition
|
|
validateNewConfigDoesntHitCache(dict: ["disabled_rules": ["nesting", "todo"]], initialConfig: initialConfig)
|
|
// Removal
|
|
validateNewConfigDoesntHitCache(dict: ["disabled_rules": []], initialConfig: initialConfig)
|
|
}
|
|
|
|
func testOptInRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
|
|
let initialConfig = Configuration(dict: ["opt_in_rules": ["attributes"]])!
|
|
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
|
|
|
|
// Change
|
|
validateNewConfigDoesntHitCache(dict: ["opt_in_rules": ["empty_count"]], initialConfig: initialConfig)
|
|
// Rules addition
|
|
validateNewConfigDoesntHitCache(dict: ["opt_in_rules": ["attributes", "empty_count"]],
|
|
initialConfig: initialConfig)
|
|
// Removal
|
|
validateNewConfigDoesntHitCache(dict: ["opt_in_rules": []], initialConfig: initialConfig)
|
|
}
|
|
|
|
func testEnabledRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
|
|
let initialConfig = Configuration(dict: ["enabled_rules": ["attributes"]])!
|
|
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
|
|
|
|
// Change
|
|
validateNewConfigDoesntHitCache(dict: ["enabled_rules": ["empty_count"]], initialConfig: initialConfig)
|
|
// Addition
|
|
validateNewConfigDoesntHitCache(dict: ["enabled_rules": ["attributes", "empty_count"]],
|
|
initialConfig: initialConfig)
|
|
// Removal
|
|
validateNewConfigDoesntHitCache(dict: ["enabled_rules": []], initialConfig: initialConfig)
|
|
}
|
|
|
|
func testWhitelistRulesChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
|
|
let initialConfig = Configuration(dict: ["whitelist_rules": ["nesting"]])!
|
|
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
|
|
|
|
// Change
|
|
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": ["todo"]], initialConfig: initialConfig)
|
|
// Addition
|
|
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": ["nesting", "todo"]], initialConfig: initialConfig)
|
|
// Removal
|
|
validateNewConfigDoesntHitCache(dict: ["whitelist_rules": []], initialConfig: initialConfig)
|
|
}
|
|
|
|
func testRuleConfigurationChangedOrAddedOrRemovedCausesAllFilesToBeReLinted() {
|
|
let initialConfig = Configuration(dict: ["line_length": 120])!
|
|
cacheAndValidateNoViolationsTwoFiles(configuration: initialConfig)
|
|
|
|
// Change
|
|
validateNewConfigDoesntHitCache(dict: ["line_length": 100], initialConfig: initialConfig)
|
|
// Addition
|
|
validateNewConfigDoesntHitCache(dict: ["line_length": 100, "number_separator": ["minimum_length": 5]],
|
|
initialConfig: initialConfig)
|
|
// Removal
|
|
validateNewConfigDoesntHitCache(dict: [:], initialConfig: initialConfig)
|
|
}
|
|
|
|
func testSwiftVersionChangedRemovedCausesAllFilesToBeReLinted() {
|
|
let fileManager = TestFileManager()
|
|
cache = LinterCache(fileManager: fileManager)
|
|
let helper = makeCacheTestHelper(dict: [:])
|
|
let file = "foo.swift"
|
|
let violations = helper.makeViolations(file: file)
|
|
|
|
cacheAndValidate(violations: violations, forFile: file, configuration: helper.configuration)
|
|
let thisSwiftVersionCache = cache
|
|
|
|
let differentSwiftVersion: SwiftVersion = (SwiftVersion.current >= .four) ? .three : .four
|
|
cache = LinterCache(fileManager: fileManager, swiftVersion: differentSwiftVersion)
|
|
|
|
XCTAssertNotNil(thisSwiftVersionCache.violations(forFile: file, configuration: helper.configuration))
|
|
XCTAssertNil(cache.violations(forFile: file, configuration: helper.configuration))
|
|
}
|
|
|
|
func testDetectSwiftVersion() {
|
|
#if swift(>=4.2.0)
|
|
let version = "4.2.0"
|
|
#elseif swift(>=4.1.1)
|
|
let version = "4.1.1"
|
|
#elseif swift(>=4.1.0)
|
|
let version = "4.1.0"
|
|
#elseif swift(>=4.0.3)
|
|
let version = "4.0.3"
|
|
#elseif swift(>=4.0.2)
|
|
let version = "4.0.2"
|
|
#elseif swift(>=4.0.1)
|
|
let version = "4.0.1"
|
|
#elseif swift(>=4.0.0)
|
|
let version = "4.0.0"
|
|
#elseif swift(>=3.4.0)
|
|
let version = "4.2.0" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.2.0
|
|
#elseif swift(>=3.3.1)
|
|
let version = "4.1.1" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.1.1
|
|
#elseif swift(>=3.3.0)
|
|
let version = "4.1.0" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.1.0
|
|
#elseif swift(>=3.2.3)
|
|
let version = "4.0.3" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.3
|
|
#elseif swift(>=3.2.2)
|
|
let version = "4.0.2" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.2
|
|
#elseif swift(>=3.2.1)
|
|
let version = "4.0.1" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.1
|
|
#else // if swift(>=3.2.0)
|
|
let version = "4.0.0" // Since we can't pass SWIFT_VERSION=3 to sourcekit, it returns 4.0.0
|
|
#endif
|
|
XCTAssertEqual(SwiftVersion.current.rawValue, version)
|
|
}
|
|
|
|
// MARK: JSON output
|
|
|
|
func testCacheToJSONDoesntCrash() {
|
|
// swiftlint:disable line_length
|
|
let key1 = "[\"/SwiftLint/source\",[[\"block_based_kvo\",\"warning\"],[\"class_delegate_protocol\",\"warning\"],[\"closing_brace\",\"warning\"],[\"closure_parameter_position\",\"warning\"],[\"colon\",\"warning, flexible_right_spacing: false, apply_to_dictionaries: true\"],[\"comma\",\"warning\"],[\"compiler_protocol_init\",\"warning\"],[\"control_statement\",\"warning\"],[\"custom_rules\",\"\"],[\"cyclomatic_complexity\",\"warning: 10, error: 20, ignores_case_statements: false\"],[\"discarded_notification_center_observer\",\"warning\"],[\"discouraged_direct_init\",\"warning, types: [\"Bundle\", \"Bundle.init\", \"UIDevice\", \"UIDevice.init\"]\"],[\"dynamic_inline\",\"error\"],[\"empty_enum_arguments\",\"warning\"],[\"empty_parameters\",\"warning\"]]]"
|
|
|
|
let key2 = "[\"/SwiftLint/Source\",[[\"block_based_kvo\",\"warning\"],[\"class_delegate_protocol\",\"warning\"],[\"closing_brace\",\"warning\"],[\"closure_parameter_position\",\"warning\"],[\"colon\",\"warning, flexible_right_spacing: false, apply_to_dictionaries: true\"],[\"comma\",\"warning\"],[\"compiler_protocol_init\",\"warning\"],[\"control_statement\",\"warning\"],[\"custom_rules\",\"\"],[\"cyclomatic_complexity\",\"warning: 10, error: 20, ignores_case_statements: false\"],[\"discarded_notification_center_observer\",\"warning\"],[\"discouraged_direct_init\",\"warning, types: [\"Bundle\", \"Bundle.init\", \"UIDevice\", \"UIDevice.init\"]\"],[\"dynamic_inline\",\"error\"],[\"empty_enum_arguments\",\"warning\"],[\"empty_parameters\",\"warning\"]]]"
|
|
// swiftlint:enable line_length
|
|
|
|
let dict = [key1: "test", key2: "test2"]
|
|
|
|
XCTAssertNoThrow(try dict.toJSON())
|
|
}
|
|
}
|