Skip to content

Commit f281814

Browse files
committed
Migrate Custom Rules from SourceKit to SwiftSyntax
**Breaking Change**: Custom rules now default to SwiftSyntax mode for pattern matching. This provides significant performance improvements but may have subtle behavioral differences from the previous SourceKit implementation. Users should test their custom rules and adjust as needed. A temporary `sourcekit` mode is available for rules that cannot be migrated yet. ## Breaking Change Details - **New Default**: Custom rules now use `swiftsyntax` mode by default - **Legacy Option**: Set `mode: sourcekit` or `default_execution_mode: sourcekit` to restore previous behavior temporarily - **Action Required**: Test your custom rules after upgrading and migrate to SwiftSyntax mode where possible ## Configuration Control execution mode at two levels: ```yaml custom_rules: # To restore previous behavior for all rules (temporary): default_execution_mode: sourcekit my_rule: regex: "pattern" # Defaults to swiftsyntax now legacy_rule: regex: "pattern" mode: sourcekit # Use legacy mode for specific rules ``` ## Why This Change? - **Performance**: SwiftSyntax mode eliminates SourceKit process overhead - **Reliability**: No more SourceKit crashes or hangs - **Future-Proof**: SwiftLint is migrating away from SourceKit entirely ## Migration Guide 1. **Test First**: Run your existing custom rules and check for differences 2. **Adjust Rules**: Most rules should work identically in SwiftSyntax mode 3. **Use Legacy Mode**: For rules that must maintain exact SourceKit behavior, set `mode: sourcekit` temporarily 4. **Report Issues**: Help us improve SwiftSyntax mode by reporting incompatibilities ## Technical Implementation ### SwiftSyntaxKindBridge Enables kind filtering (`match_kinds`/`excluded_match_kinds`) without SourceKit: - Maps SwiftSyntax classifications to SourceKit syntax kinds - Covers common use cases with best-effort compatibility - Some edge cases may differ from SourceKit behavior ### ConditionallySourceKitFree Protocol Allows CustomRules to skip SourceKit initialization when all rules use SwiftSyntax: - Dramatic performance improvement when SourceKit isn't needed - Maintains SourceKit availability for legacy rules ## Important Notes - **Not Full Compatibility**: While we've worked to maintain compatibility, differences exist. Test thoroughly. - **Temporary Legacy Support**: The `sourcekit` mode is a temporary measure. Plan to migrate all rules to SwiftSyntax. - **Future Deprecation**: SourceKit mode will be removed in a future version. ## Benefits of Migrating - Faster linting (no SourceKit startup cost) - Lower memory usage - Better reliability (no SourceKit crashes) - Future-proof your configuration This change represents a major step in SwiftLint's evolution toward a fully SwiftSyntax-based architecture. We encourage all users to test and migrate their custom rules to the new SwiftSyntax mode, using the legacy SourceKit mode only as a temporary measure for rules that cannot yet be migrated.
1 parent ab7d117 commit f281814

File tree

5 files changed

+471
-7
lines changed

5 files changed

+471
-7
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
* SwiftLint now requires macOS 13 or higher to run.
1212
[JP Simard](https://github.com/jpsim)
1313

14+
* Custom rules now default to SwiftSyntax mode for pattern matching instead of SourceKit.
15+
This may result in subtle behavioral differences. While performance is significantly improved,
16+
rules that rely on specific SourceKit behaviors may need adjustment. Users can temporarily
17+
revert to the legacy SourceKit behavior by setting `default_execution_mode: sourcekit` in
18+
their custom rules configuration or `execution_mode: sourcekit` for individual rules.
19+
The SourceKit mode is deprecated and will be removed in a future version.
20+
[JP Simard](https://github.com/jpsim)
21+
1422
### Experimental
1523

1624
* None.

Source/SwiftLintCore/Extensions/StringView+SwiftLint.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Foundation
12
import SourceKittenFramework
23

34
public extension StringView {
@@ -12,4 +13,17 @@ public extension StringView {
1213
}
1314
return lines[Int(line) - 1].byteRange.location + ByteCount(bytePosition - 1)
1415
}
16+
17+
/// Matches a pattern in the string view and returns ranges for the specified capture group.
18+
/// This method does not use SourceKit and is suitable for SwiftSyntax mode.
19+
/// - Parameters:
20+
/// - pattern: The regular expression pattern to match.
21+
/// - captureGroup: The capture group index to extract (0 for the full match).
22+
/// - Returns: An array of NSRange objects for the matched capture groups.
23+
func match(pattern: String, captureGroup: Int = 0) -> [NSRange] {
24+
regex(pattern).matches(in: self).compactMap { match in
25+
let range = match.range(at: captureGroup)
26+
return range.location != NSNotFound ? range : nil
27+
}
28+
}
1529
}

Source/SwiftLintFramework/Rules/CustomRules.swift

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import SourceKittenFramework
23

34
// MARK: - CustomRulesConfiguration
45

@@ -103,7 +104,49 @@ struct CustomRules: Rule, CacheDescriptionProvider, ConditionallySourceKitFree {
103104
let pattern = configuration.regex.pattern
104105
let captureGroup = configuration.captureGroup
105106
let excludingKinds = configuration.excludedMatchKinds
106-
return file.match(pattern: pattern, excludingSyntaxKinds: excludingKinds, captureGroup: captureGroup).map({
107+
108+
// Determine effective execution mode (defaults to swiftsyntax if not specified)
109+
let effectiveMode = configuration.executionMode == .default
110+
? (self.configuration.defaultExecutionMode ?? .swiftsyntax)
111+
: configuration.executionMode
112+
let needsKindMatching = !excludingKinds.isEmpty
113+
114+
let matches: [NSRange]
115+
if effectiveMode == .swiftsyntax {
116+
if needsKindMatching {
117+
// SwiftSyntax mode WITH kind filtering
118+
// CRITICAL: This path must not trigger any SourceKit requests
119+
guard let bridgedTokens = file.swiftSyntaxDerivedSourceKittenTokens else {
120+
// Log error/warning: Bridging failed
121+
queuedPrintError(
122+
"Warning: SwiftSyntax bridging failed for custom rule '\(configuration.identifier)'"
123+
)
124+
return []
125+
}
126+
let syntaxMapFromBridgedTokens = SwiftLintSyntaxMap(
127+
value: SyntaxMap(tokens: bridgedTokens.map(\.value))
128+
)
129+
130+
// Use the performMatchingWithSyntaxMap helper that operates on stringView and syntaxMap ONLY
131+
matches = performMatchingWithSyntaxMap(
132+
stringView: file.stringView,
133+
syntaxMap: syntaxMapFromBridgedTokens,
134+
pattern: pattern,
135+
excludingSyntaxKinds: excludingKinds,
136+
captureGroup: captureGroup
137+
)
138+
} else {
139+
// SwiftSyntax mode WITHOUT kind filtering
140+
// This path must not trigger any SourceKit requests
141+
matches = file.stringView.match(pattern: pattern, captureGroup: captureGroup)
142+
}
143+
} else {
144+
// SourceKit mode
145+
// SourceKit calls ARE EXPECTED AND PERMITTED here because CustomRules is not SourceKitFreeRule
146+
matches = file.match(pattern: pattern, excludingSyntaxKinds: excludingKinds, captureGroup: captureGroup)
147+
}
148+
149+
return matches.map({
107150
StyleViolation(ruleDescription: configuration.description,
108151
severity: configuration.severity,
109152
location: Location(file: file, characterOffset: $0.location),
@@ -134,3 +177,42 @@ struct CustomRules: Rule, CacheDescriptionProvider, ConditionallySourceKitFree {
134177
&& !region.disabledRuleIdentifiers.contains(.all)
135178
}
136179
}
180+
181+
// MARK: - Helpers
182+
183+
private func performMatchingWithSyntaxMap(
184+
stringView: StringView,
185+
syntaxMap: SwiftLintSyntaxMap,
186+
pattern: String,
187+
excludingSyntaxKinds: Set<SyntaxKind>,
188+
captureGroup: Int
189+
) -> [NSRange] {
190+
// This helper method must not access any part of SwiftLintFile that could trigger SourceKit requests
191+
// It operates only on the provided stringView and syntaxMap
192+
193+
let regex = regex(pattern)
194+
let range = stringView.range
195+
let matches = regex.matches(in: stringView, options: [], range: range)
196+
197+
return matches.compactMap { match in
198+
let matchRange = match.range(at: captureGroup)
199+
200+
// Get tokens in the match range
201+
guard let byteRange = stringView.NSRangeToByteRange(
202+
start: matchRange.location,
203+
length: matchRange.length
204+
) else {
205+
return nil
206+
}
207+
208+
let tokensInRange = syntaxMap.tokens(inByteRange: byteRange)
209+
let kindsInRange = Set(tokensInRange.kinds)
210+
211+
// Check if any excluded kinds are present
212+
if excludingSyntaxKinds.isDisjoint(with: kindsInRange) {
213+
return matchRange
214+
}
215+
216+
return nil
217+
}
218+
}

Tests/FrameworkTests/CustomRulesTests.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ final class CustomRulesTests: SwiftLintTestCase {
1111

1212
private var testFile: SwiftLintFile { SwiftLintFile(path: "\(TestResources.path())/test.txt")! }
1313

14-
override func invokeTest() {
15-
CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) {
16-
super.invokeTest()
17-
}
18-
}
19-
2014
func testCustomRuleConfigurationSetsCorrectlyWithMatchKinds() {
2115
let configDict = [
2216
"my_custom_rule": [

0 commit comments

Comments
 (0)