Skip to content

Commit 81474e3

Browse files
authored
Enforce SourceKitFreeRule contract with fatal error (#6107)
1 parent 873b2b6 commit 81474e3

File tree

10 files changed

+117
-26
lines changed

10 files changed

+117
-26
lines changed

Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ private extension SwiftLintFile {
8484
func index(compilerArguments: [String]) -> SourceKittenDictionary? {
8585
path
8686
.flatMap { path in
87-
try? Request.index(file: path, arguments: compilerArguments)
88-
.send()
87+
try? Request.index(file: path, arguments: compilerArguments).send()
8988
}
9089
.map(SourceKittenDictionary.init)
9190
}

Source/SwiftLintCore/Extensions/Request+SwiftLint.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ public extension Request {
55
static let disableSourceKit = ProcessInfo.processInfo.environment["SWIFTLINT_DISABLE_SOURCEKIT"] != nil
66

77
func sendIfNotDisabled() throws -> [String: any SourceKitRepresentable] {
8+
// Skip safety checks if explicitly allowed (e.g., for testing or specific operations)
9+
if !CurrentRule.allowSourceKitRequestWithoutRule {
10+
// Check if we have a rule context
11+
if let ruleID = CurrentRule.identifier {
12+
// Skip registry check for mock test rules
13+
if ruleID != "mock_test_rule_for_swiftlint_tests" {
14+
// Ensure the rule exists in the registry
15+
guard let ruleType = RuleRegistry.shared.rule(forID: ruleID) else {
16+
queuedFatalError("""
17+
Rule '\(ruleID)' not found in RuleRegistry. This indicates a configuration or wiring issue.
18+
""")
19+
}
20+
21+
// Check if the current rule is a SourceKitFreeRule
22+
if ruleType is any SourceKitFreeRule.Type {
23+
queuedFatalError("""
24+
'\(ruleID)' is a SourceKitFreeRule and should not be making requests to SourceKit.
25+
""")
26+
}
27+
}
28+
} else {
29+
// No rule context and not explicitly allowed
30+
queuedFatalError("""
31+
SourceKit request made outside of rule execution context without explicit permission.
32+
Use CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { ... } for allowed exceptions.
33+
""")
34+
}
35+
}
36+
837
guard !Self.disableSourceKit else {
938
throw Self.Error.connectionInterrupted("SourceKit is disabled by `SWIFTLINT_DISABLE_SOURCEKIT`.")
1039
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// A task-local value that holds the identifier of the currently executing rule.
2+
/// This allows SourceKit request handling to determine if the current rule
3+
/// is a SourceKitFreeRule without modifying function signatures throughout the codebase.
4+
public enum CurrentRule {
5+
/// The Rule ID for the currently executing rule.
6+
@TaskLocal public static var identifier: String?
7+
8+
/// Allows specific SourceKit requests to be made outside of rule execution context.
9+
/// This should only be used for essential operations like getting the Swift version.
10+
@TaskLocal public static var allowSourceKitRequestWithoutRule = false
11+
}

Source/SwiftLintCore/Models/SwiftVersion.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ public extension SwiftVersion {
8787
if !Request.disableSourceKit {
8888
// This request was added in Swift 5.1
8989
let params: SourceKitObject = ["key.request": UID("source.request.compiler_version")]
90-
if let result = try? Request.customRequest(request: params).send(),
90+
// Allow this specific SourceKit request outside of rule execution context
91+
let result = CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) {
92+
try? Request.customRequest(request: params).sendIfNotDisabled()
93+
}
94+
if let result,
9195
let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch {
9296
return SwiftVersion(rawValue: "\(major).\(minor).\(patch)")
9397
}

Source/SwiftLintFramework/Models/Linter.swift

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import SourceKittenFramework
3+
import SwiftLintCore
34

45
// swiftlint:disable file_length
56

@@ -95,6 +96,32 @@ private extension Rule {
9596
compilerArguments: [String]) -> LintResult {
9697
let ruleID = Self.identifier
9798

99+
// Wrap entire lint process including shouldRun check in rule context
100+
return CurrentRule.$identifier.withValue(ruleID) {
101+
guard shouldRun(onFile: file) else {
102+
return LintResult(violations: [], ruleTime: nil, deprecatedToValidIDPairs: [])
103+
}
104+
105+
return performLint(
106+
file: file,
107+
regions: regions,
108+
benchmark: benchmark,
109+
storage: storage,
110+
superfluousDisableCommandRule: superfluousDisableCommandRule,
111+
compilerArguments: compilerArguments
112+
)
113+
}
114+
}
115+
116+
// swiftlint:disable:next function_parameter_count
117+
private func performLint(file: SwiftLintFile,
118+
regions: [Region],
119+
benchmark: Bool,
120+
storage: RuleStorage,
121+
superfluousDisableCommandRule: SuperfluousDisableCommandRule?,
122+
compilerArguments: [String]) -> LintResult {
123+
let ruleID = Self.identifier
124+
98125
let violations: [StyleViolation]
99126
let ruleTime: (String, Double)?
100127
if benchmark {
@@ -116,11 +143,11 @@ private extension Rule {
116143
}
117144

118145
let customRulesIDs: [String] = {
119-
guard let customRules = self as? CustomRules else {
120-
return []
121-
}
122-
return customRules.customRuleIdentifiers
123-
}()
146+
guard let customRules = self as? CustomRules else {
147+
return []
148+
}
149+
return customRules.customRuleIdentifiers
150+
}()
124151
let ruleIDs = Self.description.allIdentifiers +
125152
customRulesIDs +
126153
(superfluousDisableCommandRule.map({ type(of: $0) })?.description.allIdentifiers ?? []) +
@@ -247,7 +274,11 @@ public struct Linter {
247274
/// - returns: A linter capable of checking for violations after running each rule's collection step.
248275
public func collect(into storage: RuleStorage) -> CollectedLinter {
249276
DispatchQueue.concurrentPerform(iterations: rules.count) { idx in
250-
rules[idx].collectInfo(for: file, into: storage, compilerArguments: compilerArguments)
277+
let rule = rules[idx]
278+
let ruleID = type(of: rule).identifier
279+
CurrentRule.$identifier.withValue(ruleID) {
280+
rule.collectInfo(for: file, into: storage, compilerArguments: compilerArguments)
281+
}
251282
}
252283
return CollectedLinter(from: self)
253284
}
@@ -306,15 +337,11 @@ public struct CollectedLinter {
306337
let superfluousDisableCommandRule = rules.first(where: {
307338
$0 is SuperfluousDisableCommandRule
308339
}) as? SuperfluousDisableCommandRule
309-
let validationResults: [LintResult] = rules.parallelCompactMap {
310-
guard $0.shouldRun(onFile: file) else {
311-
return nil
312-
}
313-
314-
return $0.lint(file: file, regions: regions, benchmark: benchmark,
315-
storage: storage,
316-
superfluousDisableCommandRule: superfluousDisableCommandRule,
317-
compilerArguments: compilerArguments)
340+
let validationResults: [LintResult] = rules.parallelMap {
341+
$0.lint(file: file, regions: regions, benchmark: benchmark,
342+
storage: storage,
343+
superfluousDisableCommandRule: superfluousDisableCommandRule,
344+
compilerArguments: compilerArguments)
318345
}
319346
let undefinedSuperfluousCommandViolations = self.undefinedSuperfluousCommandViolations(
320347
regions: regions, configuration: configuration,
@@ -381,17 +408,20 @@ public struct CollectedLinter {
381408
}
382409

383410
var corrections = [String: Int]()
384-
for rule in rules where rule.shouldRun(onFile: file) {
385-
guard let rule = rule as? any CorrectableRule else {
386-
continue
411+
for rule in rules.compactMap({ $0 as? any CorrectableRule }) {
412+
// Set rule context before checking shouldRun to allow file property access
413+
let ruleCorrections = CurrentRule.$identifier.withValue(type(of: rule).identifier) { () -> Int? in
414+
guard rule.shouldRun(onFile: file) else {
415+
return nil
416+
}
417+
return rule.correct(file: file, using: storage, compilerArguments: compilerArguments)
387418
}
388-
let corrected = rule.correct(file: file, using: storage, compilerArguments: compilerArguments)
389-
if corrected != 0 {
419+
if let corrected = ruleCorrections, corrected != 0 {
390420
corrections[type(of: rule).description.identifier] = corrected
391421
if !file.isVirtual {
392422
file.invalidateCache()
393423
}
394-
}
424+
}
395425
}
396426
return corrections
397427
}

Tests/BuiltInRulesTests/FileHeaderRuleTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import XCTest
55
private let fixturesDirectory = "\(TestResources.path())/FileHeaderRuleFixtures"
66

77
final class FileHeaderRuleTests: SwiftLintTestCase {
8+
override func invokeTest() {
9+
CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) {
10+
super.invokeTest()
11+
}
12+
}
13+
814
private func validate(fileName: String, using configuration: Any) throws -> [StyleViolation] {
915
let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))!
1016
let rule = try FileHeaderRule(configuration: configuration)

Tests/FrameworkTests/CollectingRuleTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ extension MockCollectingRule {
136136
@RuleConfigurationDescriptionBuilder
137137
var configurationDescription: some Documentable { RuleConfigurationOption.noOptions }
138138
static var description: RuleDescription {
139-
RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint)
139+
RuleDescription(identifier: "mock_test_rule_for_swiftlint_tests", name: "", description: "", kind: .lint)
140140
}
141141
static var configuration: Configuration? {
142142
Configuration(rulesMode: .onlyConfiguration([identifier]), ruleList: RuleList(rules: self))

Tests/FrameworkTests/CustomRulesTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ 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+
1420
func testCustomRuleConfigurationSetsCorrectlyWithMatchKinds() {
1521
let configDict = [
1622
"my_custom_rule": [

Tests/FrameworkTests/EmptyFileTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ private struct DontLintEmptyFiles: ShouldLintEmptyFilesProtocol {
3939
static var shouldLintEmptyFiles: Bool { false }
4040
}
4141

42-
private struct RuleMock<ShouldLintEmptyFiles: ShouldLintEmptyFilesProtocol>: CorrectableRule {
42+
private struct RuleMock<ShouldLintEmptyFiles: ShouldLintEmptyFilesProtocol>: CorrectableRule, SourceKitFreeRule {
4343
var configuration = SeverityConfiguration<Self>(.warning)
4444

4545
static var description: RuleDescription {

Tests/FrameworkTests/SourceKitCrashTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
import XCTest
33

44
final class SourceKitCrashTests: SwiftLintTestCase {
5+
override func invokeTest() {
6+
CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) {
7+
super.invokeTest()
8+
}
9+
}
10+
511
func testAssertHandlerIsNotCalledOnNormalFile() {
612
let file = SwiftLintFile(contents: "A file didn't crash SourceKitService")
713
file.sourcekitdFailed = false

0 commit comments

Comments
 (0)