Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b55546e
Rewrite `custom_rules` with SwiftSyntax
jpsim Oct 13, 2022
9978204
Annotate some rules as SourceKitFreeRule
jpsim Oct 17, 2022
5535a86
Fix `throws` handling
jpsim Oct 18, 2022
998f595
Add repro case for DiscouragedOptionalCollection regression
jpsim Oct 19, 2022
98973cf
Add more rules to changelog
jpsim Oct 19, 2022
ca8573c
fixup! Add more rules to changelog
jpsim Oct 19, 2022
5294a32
Handle some whitespace differences
jpsim Oct 19, 2022
b8f1bd8
Fixes & Hacks
jpsim Oct 19, 2022
0395692
Fix commented out code
jpsim Oct 20, 2022
69833d1
WIP: Always get tokens from SwiftSyntax
jpsim Oct 20, 2022
8643307
DEBUG
jpsim Oct 20, 2022
32f3b77
One more fix
jpsim Oct 20, 2022
5661f70
More fixes
jpsim Oct 20, 2022
46645cc
One more hack
jpsim Oct 20, 2022
4754343
Guess what, yet again more hacks
jpsim Oct 20, 2022
115ee6c
omg hacks
jpsim Oct 20, 2022
8f5a6d5
omg this is absurd
jpsim Oct 20, 2022
51fa671
Ignore comment URL kinds
jpsim Oct 20, 2022
95070b2
what's one more hack?
jpsim Oct 20, 2022
2c407b5
Only all tokens
jpsim Oct 20, 2022
b77c352
handle special comment kinds in comparisons
jpsim Oct 20, 2022
8da6c1e
boolean logic is hard
jpsim Oct 20, 2022
18fc9af
Remove debug code
jpsim Oct 20, 2022
54f7ed0
Remove optional
jpsim Oct 20, 2022
0fa0fbd
Fix linter violations
jpsim Oct 20, 2022
cb3af20
Remove extra type annotation
jpsim Oct 20, 2022
d86fc8f
Remove debug code
jpsim Oct 20, 2022
682b366
Reduce scope of linter disables
jpsim Oct 20, 2022
d0f60fa
Fix test
jpsim Oct 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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`
Expand Down Expand Up @@ -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`
Expand Down
13 changes: 2 additions & 11 deletions Source/SwiftLintFramework/Extensions/SwiftLintFile+Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down Expand Up @@ -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) }

Expand Down
192 changes: 192 additions & 0 deletions Source/SwiftLintFramework/Helpers/SwiftSyntaxSourceKitBridge.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<K, V>(_ dict1: [K: V], ↓_ dict2: [K: V]?) -> [K: V]"),
wrapExample("enum", "static func foo<K, V>(dict1: [K: V], ↓dict2: [K: V]?) -> [K: V]")
wrapExample("enum", "static func foo<K, V>(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<Self>? = nil) -> [Int: Self]? {

let cache = cache ?? HomeWidgetCache<Self>(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<Self>? = nil) {

let cache = cache ?? HomeWidgetCache<Self>(fileName: Self.filename,
appGroup: WPAppGroupName)

do {
try cache.write(items: items)
} catch {
DDLogError("HomeWidgetToday: Failed writing data: \(error.localizedDescription)")
}
}

static func delete(cache: HomeWidgetCache<Self>? = nil) {
let cache = cache ?? HomeWidgetCache<Self>(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<Self>? = nil) {
let cache = cache ?? HomeWidgetCache<Self>(fileName: Self.filename,
appGroup: WPAppGroupName)

do {
try cache.setItem(item: item)
} catch {
DDLogError("HomeWidgetToday: Failed writing data item: \(error.localizedDescription)")
}
}
}
"""#, excludeFromDocumentation: true)
]
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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*"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Source/SwiftLintFramework/Rules/Style/CustomRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
3 changes: 2 additions & 1 deletion Source/SwiftLintFramework/Rules/Style/VoidReturnRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 2 additions & 2 deletions Tests/SwiftLintFrameworkTests/SourceKitCrashTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down