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