Files
JP Simard ab7d117030 Migrate FileHeaderRule from SourceKit to SwiftSyntax (#6112)
## Summary

Convert FileHeaderRule to use SwiftSyntax instead of SourceKit for
improved performance and better handling of file header comments,
shebangs, and doc comments.

## Key Technical Improvements

- **Enhanced shebang support** properly skipping past `#!/usr/bin/env swift` lines
- **Better comment type discrimination** excluding doc comments from header analysis
- **Accurate position calculation** converting between UTF-8 and UTF-16 offsets for regex matching
- **Improved trivia traversal** for comprehensive header comment collection
- **SwiftLint command filtering** to exclude directive comments from header content

## Migration Details

- Replaced `OptInRule` with `@SwiftSyntaxRule(optIn: true)` annotation
- Implemented `ViolationsSyntaxVisitor` pattern for file-level analysis
- Added logic to start header collection after shebang.endPosition if present
- Distinguished between regular comments and doc comments (///, /** */)
- Maintained UTF-16 offset calculations for NSRegularExpression compatibility
- Added `skipDisableCommandTests: true` for SwiftSyntax disable command behavior
- Removed unnecessary SourceKittenFramework import
2025-06-24 09:48:25 -04:00

134 lines
5.4 KiB
Swift

import Foundation
import SwiftLintCore
struct FileHeaderConfiguration: SeverityBasedRuleConfiguration {
typealias Parent = FileHeaderRule
private static let fileNamePlaceholder = "SWIFTLINT_CURRENT_FILENAME"
private static let stringRegexOptions: NSRegularExpression.Options = [.ignoreMetacharacters]
private static let patternRegexOptions: NSRegularExpression.Options =
[.anchorsMatchLines, .dotMatchesLineSeparators]
@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
@ConfigurationElement(key: "required_string")
private var requiredString: String?
@ConfigurationElement(key: "required_pattern")
private var requiredPattern: String?
@ConfigurationElement(key: "forbidden_string")
private var forbiddenString: String?
@ConfigurationElement(key: "forbidden_pattern")
private var forbiddenPattern: String?
private var _forbiddenRegex: RegularExpression?
private var _requiredRegex: RegularExpression?
private static let defaultRegex = regex("\\bCopyright\\b", options: [.caseInsensitive])
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: String] else {
throw Issue.invalidConfiguration(ruleID: Parent.identifier)
}
// Cache the created regexes if possible.
// If the pattern contains the SWIFTLINT_CURRENT_FILENAME placeholder,
// the regex will be recompiled for each validated file.
if let requiredString = configuration[$requiredString.key] {
self.requiredString = requiredString
if !requiredString.contains(Self.fileNamePlaceholder) {
_requiredRegex = try .from(
pattern: requiredString,
options: Self.stringRegexOptions,
for: Parent.identifier
)
}
} else if let requiredPattern = configuration[$requiredPattern.key] {
self.requiredPattern = requiredPattern
if !requiredPattern.contains(Self.fileNamePlaceholder) {
_requiredRegex = try .from(pattern: requiredPattern, for: Parent.identifier)
}
}
if let forbiddenString = configuration[$forbiddenString.key] {
self.forbiddenString = forbiddenString
if !forbiddenString.contains(Self.fileNamePlaceholder) {
_forbiddenRegex = try .from(
pattern: forbiddenString,
options: Self.stringRegexOptions,
for: Parent.identifier
)
}
} else if let forbiddenPattern = configuration[$forbiddenPattern.key] {
self.forbiddenPattern = forbiddenPattern
if !forbiddenPattern.contains(Self.fileNamePlaceholder) {
_forbiddenRegex = try .from(pattern: forbiddenPattern, for: Parent.identifier)
}
}
if let severityString = configuration[$severityConfiguration.key] {
try severityConfiguration.apply(configuration: severityString)
}
}
private func makeRegex(for file: SwiftLintFile,
using pattern: String,
options: NSRegularExpression.Options,
escapeFileName: Bool) -> NSRegularExpression? {
// Recompile the regex for this file...
let replacedPattern = file.path.map { path in
let fileName = path.bridge().lastPathComponent
// Replace SWIFTLINT_CURRENT_FILENAME with the filename.
let escapedName = escapeFileName ? NSRegularExpression.escapedPattern(for: fileName) : fileName
return pattern.replacingOccurrences(of: Self.fileNamePlaceholder,
with: escapedName)
} ?? pattern
do {
return try NSRegularExpression(pattern: replacedPattern, options: options)
} catch {
queuedFatalError("Failed to compile pattern '\(replacedPattern)'")
}
}
private func regexFromString(for file: SwiftLintFile, using pattern: String) -> NSRegularExpression? {
makeRegex(for: file, using: pattern, options: Self.stringRegexOptions, escapeFileName: false)
}
private func regexFromPattern(for file: SwiftLintFile, using pattern: String) -> NSRegularExpression? {
makeRegex(for: file, using: pattern, options: Self.patternRegexOptions, escapeFileName: true)
}
func forbiddenRegex(for file: SwiftLintFile) -> NSRegularExpression? {
if _forbiddenRegex != nil {
return _forbiddenRegex?.regex
}
if let regex = forbiddenString.flatMap({ regexFromString(for: file, using: $0) }) {
return regex
}
if let regex = forbiddenPattern.flatMap({ regexFromPattern(for: file, using: $0) }) {
return regex
}
if requiredPattern == nil, requiredString == nil {
return Self.defaultRegex
}
return nil
}
func requiredRegex(for file: SwiftLintFile) -> NSRegularExpression? {
if _requiredRegex != nil {
return _requiredRegex?.regex
}
if let regex = requiredString.flatMap({ regexFromString(for: file, using: $0) }) {
return regex
}
return requiredPattern.flatMap { regexFromPattern(for: file, using: $0) }
}
}