diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d1500d5f..76a5732f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - `contains_over_filter_is_empty` - `contains_over_first_not_nil` - `contains_over_range_nil_comparison` + - `custom_rules` - `deployment_target` - `discouraged_assert` - `discouraged_direct_init` @@ -70,6 +71,7 @@ - `empty_parentheses_with_trailing_closure` - `empty_string` - `enum_case_associated_values_count` + - `expiring_todo` - `explicit_enum_raw_value` - `explicit_init` - `fallthrough` @@ -106,11 +108,12 @@ - `nsobject_prefer_isequal` - `number_separator` - `operator_whitespace` - - `nsobject_prefer_isequal` - `prefer_nimble` + - `prefer_self_type_over_type_of_self` + - `prefer_zero_over_explicit_init` - `private_action` - - `private_over_fileprivate` - `private_outlet` + - `private_over_fileprivate` - `private_unit_test` - `prohibited_interface_builder` - `protocol_property_accessors_order` diff --git a/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift b/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift index f13ec0c37f..81599099d5 100644 --- a/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift +++ b/Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift @@ -34,7 +34,7 @@ private let commandsCache = Cache { file -> [Command] in .walk(file: file, handler: \.commands) } private let syntaxMapCache = Cache { file in - responseCache.get(file).map { SwiftLintSyntaxMap(value: SyntaxMap(sourceKitResponse: $0)) } + SwiftLintSyntaxMap(value: SyntaxMap(tokens: SwiftSyntaxSourceKitBridge.tokens(file: file).map(\.value))) } private let syntaxKindsByLinesCache = Cache { file in file.syntaxKindsByLine() } private let syntaxTokensByLinesCache = Cache { file in file.syntaxTokensByLine() } @@ -147,16 +147,7 @@ extension SwiftLintFile { return structureDictionary } - internal var syntaxMap: SwiftLintSyntaxMap { - guard let syntaxMap = syntaxMapCache.get(self) else { - if let handler = assertHandler { - handler() - return SwiftLintSyntaxMap(value: SyntaxMap(data: [])) - } - queuedFatalError("Never call this for file that sourcekitd fails.") - } - return syntaxMap - } + internal var syntaxMap: SwiftLintSyntaxMap { syntaxMapCache.get(self) } internal var syntaxTree: SourceFileSyntax { syntaxTreeCache.get(self) } diff --git a/Source/SwiftLintFramework/Helpers/SwiftSyntaxSourceKitBridge.swift b/Source/SwiftLintFramework/Helpers/SwiftSyntaxSourceKitBridge.swift new file mode 100644 index 0000000000..344cef8589 --- /dev/null +++ b/Source/SwiftLintFramework/Helpers/SwiftSyntaxSourceKitBridge.swift @@ -0,0 +1,192 @@ +import SourceKittenFramework +import SwiftSyntax + +// This file contains a pile of hacks in order to convert the syntax classifications provided by SwiftSyntax to the +// data that SourceKit used to provide. + +/// Holds code to bridge SwiftSyntax concepts to SourceKit concepts. +enum SwiftSyntaxSourceKitBridge { + static func tokens(file: SwiftLintFile) -> [SwiftLintSyntaxToken] { + file.allTokens() + } +} + +// MARK: - Private + +private extension SwiftLintFile { + // swiftlint:disable:next cyclomatic_complexity function_body_length + func allTokens() -> [SwiftLintSyntaxToken] { + let visitor = QuoteVisitor(viewMode: .sourceAccurate) + let syntaxTree = self.syntaxTree + visitor.walk(syntaxTree) + let openQuoteRanges = visitor.openQuoteRanges.sorted(by: { $0.offset < $1.offset }) + let closeQuoteRanges = visitor.closeQuoteRanges.sorted(by: { $0.offset < $1.offset }) + let classifications = Array(syntaxTree.classifications) + let new1 = classifications.enumerated().compactMap { index, classification -> SwiftLintSyntaxToken? in + guard var syntaxKind = classification.kind.toSyntaxKind() else { + return nil + } + + var offset = ByteCount(classification.offset) + var length = ByteCount(classification.length) + + let lastCharRange = ByteRange(location: offset + length, length: 1) + if syntaxKind.isCommentLike, + classification.kind != .docBlockComment, + classification.kind != .blockComment, + index != classifications.count - 1, // Don't adjust length if this is the last classification + stringView.substringWithByteRange(lastCharRange)?.allSatisfy(\.isNewline) == true { + length += 1 + } else if syntaxKind == .string { + if let openQuote = openQuoteRanges.first(where: { $0.intersectsOrTouches(classification.range) }) { + let diff = offset - ByteCount(openQuote.offset) + offset = ByteCount(openQuote.offset) + length += diff + } + + if let closeQuote = closeQuoteRanges.first(where: { $0.intersectsOrTouches(classification.range) }) { + length = ByteCount(closeQuote.endOffset) - offset + } + } + + if syntaxKind == .keyword, + case let byteRange = ByteRange(location: offset, length: length), + let substring = stringView.substringWithByteRange(byteRange) { + if substring == "Self" { + // SwiftSyntax considers 'Self' a keyword, but SourceKit considers it a type identifier. + syntaxKind = .typeidentifier + } else if ["unavailable", "swift", "deprecated", "introduced"].contains(substring) { + // SwiftSyntax considers 'unavailable' & 'swift' a keyword, but SourceKit considers it an + // identifier. + syntaxKind = .identifier + } else if substring == "throws" { + // SwiftSyntax considers `throws` a keyword, but SourceKit ignores it. + return nil + } else if AccessControlLevel(description: substring) != nil || substring == "final" || + substring == "lazy" || substring == "convenience" { + // SwiftSyntax considers ACL keywords as keywords, but SourceKit considers them to be built-in + // attributes. + syntaxKind = .attributeBuiltin + } else if substring == "for" && stringView.substringWithByteRange(lastCharRange) == ":" { + syntaxKind = .identifier + } + } + + if classification.kind == .poundDirectiveKeyword, + case let byteRange = ByteRange(location: offset, length: length), + let substring = stringView.substringWithByteRange(byteRange), + substring == "#warning" || substring == "#error" { + syntaxKind = .poundDirectiveKeyword + } + + let syntaxToken = SyntaxToken(type: syntaxKind.rawValue, offset: offset, length: length) + return SwiftLintSyntaxToken(value: syntaxToken) + } + + // Combine `@` with next keyword + var new: [SwiftLintSyntaxToken] = [] + var eatNext = false + for (index, asdf) in new1.enumerated() { + if eatNext { + let previous = new.removeLast() + let newToken = SwiftLintSyntaxToken( + value: SyntaxToken( + type: previous.value.type, + offset: previous.offset, + length: previous.length + asdf.length + ) + ) + new.append(newToken) + eatNext = false + } else if asdf.kind == .attributeBuiltin && asdf.length == 1 && new1[index + 1].kind == .keyword { + eatNext = true + new.append(asdf) + } else if asdf.kind == .attributeBuiltin && asdf.length == 1 && new1[index + 1].kind == .typeidentifier { + continue + } else { + new.append(asdf) + } + } + + return new + } +} + +private extension SyntaxClassification { + // swiftlint:disable:next cyclomatic_complexity + func toSyntaxKind() -> SyntaxKind? { + switch self { + case .none: + return nil + case .keyword: + return .keyword + case .identifier: + return .identifier + case .typeIdentifier: + return .typeidentifier + case .dollarIdentifier: + return .identifier + case .integerLiteral: + return .number + case .floatingLiteral: + return .number + case .stringLiteral: + return .string + case .stringInterpolationAnchor: + return .stringInterpolationAnchor + case .buildConfigId: + return .buildconfigID + case .poundDirectiveKeyword: + return .buildconfigKeyword + case .attribute: + return .attributeBuiltin + case .objectLiteral: + return .objectLiteral + case .editorPlaceholder: + return .placeholder + case .lineComment: + return .comment + case .docLineComment: + return .docComment + case .blockComment: + return .comment + case .docBlockComment: + return .docComment + case .operatorIdentifier: + return nil + } + } +} + +private final class QuoteVisitor: SyntaxVisitor { + var openQuoteRanges: [ByteSourceRange] = [] + var closeQuoteRanges: [ByteSourceRange] = [] + + override func visitPost(_ node: StringLiteralExprSyntax) { + if let openDelimiter = node.openDelimiter { + let offset = openDelimiter.positionAfterSkippingLeadingTrivia.utf8Offset + let end = node.openQuote.endPosition.utf8Offset + openQuoteRanges.append(ByteSourceRange(offset: offset, length: end - offset)) + } else { + let offset = node.openQuote.positionAfterSkippingLeadingTrivia.utf8Offset + let range = ByteSourceRange( + offset: offset, + length: node.openQuote.endPositionBeforeTrailingTrivia.utf8Offset - offset + ) + openQuoteRanges.append(range) + } + + if let closeDelimiter = node.closeDelimiter { + let offset = node.closeQuote.position.utf8Offset + let end = closeDelimiter.endPositionBeforeTrailingTrivia.utf8Offset + closeQuoteRanges.append(ByteSourceRange(offset: offset, length: end - offset)) + } else { + let offset = node.closeQuote.position.utf8Offset + let range = ByteSourceRange( + offset: offset, + length: node.closeQuote.endPositionBeforeTrailingTrivia.utf8Offset - offset + ) + closeQuoteRanges.append(range) + } + } +} diff --git a/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift b/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift index 99e490de21..bfb61138b2 100644 --- a/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift +++ b/Source/SwiftLintFramework/Models/SwiftLintSyntaxToken.swift @@ -1,7 +1,7 @@ import SourceKittenFramework /// A SwiftLint-aware Swift syntax token. -public struct SwiftLintSyntaxToken { +public struct SwiftLintSyntaxToken: Equatable { /// The raw `SyntaxToken` obtained by SourceKitten. public let value: SyntaxToken diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift b/Source/SwiftLintFramework/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift index 4a798ea359..081d6ceac3 100644 --- a/Source/SwiftLintFramework/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift +++ b/Source/SwiftLintFramework/Rules/Idiomatic/DiscouragedOptionalCollectionExamples.swift @@ -204,7 +204,72 @@ internal struct DiscouragedOptionalCollectionExamples { wrapExample("enum", "static func foo(↓input: [String: [String: String]?]) {}"), wrapExample("enum", "static func foo(↓↓input: [String: [String: String]?]?) {}"), wrapExample("enum", "static func foo(_ dict1: [K: V], ↓_ dict2: [K: V]?) -> [K: V]"), - wrapExample("enum", "static func foo(dict1: [K: V], ↓dict2: [K: V]?) -> [K: V]") + wrapExample("enum", "static func foo(dict1: [K: V], ↓dict2: [K: V]?) -> [K: V]"), + Example(#""" + import WidgetKit + + protocol HomeWidgetData: Codable { + + var siteID: Int { get } + var siteName: String { get } + var url: String { get } + var timeZone: TimeZone { get } + var date: Date { get } + + static var filename: String { get } + } + + + // MARK: - Local cache + extension HomeWidgetData { + + static func ↓read(from cache: HomeWidgetCache? = nil) -> [Int: Self]? { + + let cache = cache ?? HomeWidgetCache(fileName: Self.filename, + appGroup: WPAppGroupName) + do { + return try cache.read() + } catch { + DDLogError("HomeWidgetToday: Failed loading data: \(error.localizedDescription)") + return nil + } + } + + static func write(items: [Int: Self], to cache: HomeWidgetCache? = nil) { + + let cache = cache ?? HomeWidgetCache(fileName: Self.filename, + appGroup: WPAppGroupName) + + do { + try cache.write(items: items) + } catch { + DDLogError("HomeWidgetToday: Failed writing data: \(error.localizedDescription)") + } + } + + static func delete(cache: HomeWidgetCache? = nil) { + let cache = cache ?? HomeWidgetCache(fileName: Self.filename, + appGroup: WPAppGroupName) + + do { + try cache.delete() + } catch { + DDLogError("HomeWidgetToday: Failed deleting data: \(error.localizedDescription)") + } + } + + static func setItem(item: Self, to cache: HomeWidgetCache? = nil) { + let cache = cache ?? HomeWidgetCache(fileName: Self.filename, + appGroup: WPAppGroupName) + + do { + try cache.setItem(item: item) + } catch { + DDLogError("HomeWidgetToday: Failed writing data item: \(error.localizedDescription)") + } + } + } + """#, excludeFromDocumentation: true) ] } diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift b/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift index aa60e36b8b..488620be8d 100644 --- a/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift +++ b/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift @@ -1,7 +1,8 @@ import Foundation import SourceKittenFramework -public struct PreferZeroOverExplicitInitRule: OptInRule, ConfigurationProviderRule, SubstitutionCorrectableRule { +public struct PreferZeroOverExplicitInitRule: OptInRule, ConfigurationProviderRule, SubstitutionCorrectableRule, + SourceKitFreeRule { public var configuration = SeverityConfiguration(.warning) private var pattern: String { let zero = "\\s*:\\s*0(\\.0*)?\\s*" diff --git a/Source/SwiftLintFramework/Rules/Lint/ExpiringTodoRule.swift b/Source/SwiftLintFramework/Rules/Lint/ExpiringTodoRule.swift index 0b5a36c270..012ad0ebb7 100644 --- a/Source/SwiftLintFramework/Rules/Lint/ExpiringTodoRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/ExpiringTodoRule.swift @@ -1,7 +1,7 @@ import Foundation import SourceKittenFramework -public struct ExpiringTodoRule: ConfigurationProviderRule, OptInRule { +public struct ExpiringTodoRule: ConfigurationProviderRule, OptInRule, SourceKitFreeRule { enum ExpiryViolationLevel { case approachingExpiry case expired diff --git a/Source/SwiftLintFramework/Rules/Style/CustomRules.swift b/Source/SwiftLintFramework/Rules/Style/CustomRules.swift index b9acd5a048..e96b87c20a 100644 --- a/Source/SwiftLintFramework/Rules/Style/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/Style/CustomRules.swift @@ -66,7 +66,7 @@ public struct CustomRulesConfiguration: RuleConfiguration, Equatable, CacheDescr // MARK: - CustomRules -public struct CustomRules: Rule, ConfigurationProviderRule, CacheDescriptionProvider { +public struct CustomRules: Rule, ConfigurationProviderRule, CacheDescriptionProvider, SourceKitFreeRule { internal var cacheDescription: String { return configuration.cacheDescription } diff --git a/Source/SwiftLintFramework/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift b/Source/SwiftLintFramework/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift index 56a8d63add..746435e6e0 100644 --- a/Source/SwiftLintFramework/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/PreferSelfTypeOverTypeOfSelfRule.swift @@ -1,7 +1,8 @@ import Foundation import SourceKittenFramework -public struct PreferSelfTypeOverTypeOfSelfRule: OptInRule, ConfigurationProviderRule, SubstitutionCorrectableRule { +public struct PreferSelfTypeOverTypeOfSelfRule: OptInRule, ConfigurationProviderRule, SubstitutionCorrectableRule, + SourceKitFreeRule { public var configuration = SeverityConfiguration(.warning) public static let description = RuleDescription( diff --git a/Source/SwiftLintFramework/Rules/Style/VoidReturnRule.swift b/Source/SwiftLintFramework/Rules/Style/VoidReturnRule.swift index c094995b16..d9e03fe724 100644 --- a/Source/SwiftLintFramework/Rules/Style/VoidReturnRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/VoidReturnRule.swift @@ -27,7 +27,8 @@ public struct VoidReturnRule: ConfigurationProviderRule, SubstitutionCorrectable Example("func foo(completion: () -> ↓())\n"), Example("func foo(completion: () -> ↓( ))\n"), Example("func foo(completion: () -> ↓(Void))\n"), - Example("let foo: (ConfigurationTests) -> () throws -> ↓()\n") + Example("let foo: (ConfigurationTests) -> () throws -> ↓()\n"), + Example("typealias ReplaceEditorCallback = (EditorViewController, EditorViewController) -> ↓()") ], corrections: [ Example("let abc: () -> ↓() = {}\n"): Example("let abc: () -> Void = {}\n"), diff --git a/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift b/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift index e3e092b24a..54eea136bd 100644 --- a/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift +++ b/Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift @@ -42,8 +42,8 @@ class SourceKitCrashTests: XCTestCase { assertHandlerCalled = false _ = file.syntaxMap - XCTAssertTrue(assertHandlerCalled, - "Expects assert handler was called on accessing SwiftLintFile.syntaxMap") + XCTAssertFalse(assertHandlerCalled, + "Expects assert handler was not called on accessing SwiftLintFile.syntaxMap") assertHandlerCalled = false _ = file.syntaxKindsByLines