mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Migrate FileLengthRule from SourceKit to SwiftSyntax (#6100)
Convert FileLengthRule to use SwiftSyntax instead of SourceKit for improved performance and fewer false positives. The SwiftSyntax implementation: - Uses ViolationsSyntaxVisitor pattern with token traversal - Correctly handles multiline tokens by counting all spanned lines - Properly excludes comment-only and whitespace-only lines - Accurately attributes trivia to correct line positions - Extracts common line range logic to reduce code duplication
This commit is contained in:
@@ -18,6 +18,10 @@
|
||||
[imsonalbajaj](https://github.com/imsonalbajaj)
|
||||
[#6054](https://github.com/realm/SwiftLint/issues/6054)
|
||||
|
||||
* Migrate `file_length` rule from SourceKit to SwiftSyntax for improved performance
|
||||
and fewer false positives.
|
||||
[JP Simard](https://github.com/jpsim)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Improved error reporting when SwiftLint exits, because of an invalid configuration file
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import SourceKittenFramework
|
||||
import SwiftSyntax
|
||||
|
||||
@SwiftSyntaxRule
|
||||
struct FileLengthRule: Rule {
|
||||
var configuration = FileLengthConfiguration()
|
||||
|
||||
@@ -17,38 +18,85 @@ struct FileLengthRule: Rule {
|
||||
Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()),
|
||||
].skipWrappingInCommentTests()
|
||||
)
|
||||
}
|
||||
|
||||
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||
func lineCountWithoutComments() -> Int {
|
||||
let commentKinds = SyntaxKind.commentKinds
|
||||
return file.syntaxKindsByLines.filter { kinds in
|
||||
!Set(kinds).isSubset(of: commentKinds)
|
||||
}.count
|
||||
private extension FileLengthRule {
|
||||
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
||||
override func visitPost(_ node: SourceFileSyntax) {
|
||||
let lineCount = configuration.ignoreCommentOnlyLines ? countNonCommentLines(in: node) : file.lines.count
|
||||
|
||||
let severity: ViolationSeverity, upperBound: Int
|
||||
if let error = configuration.severityConfiguration.error, lineCount > error {
|
||||
severity = .error
|
||||
upperBound = error
|
||||
} else if lineCount > configuration.severityConfiguration.warning {
|
||||
severity = .warning
|
||||
upperBound = configuration.severityConfiguration.warning
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let reason = "File should contain \(upperBound) lines or less" +
|
||||
(configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") +
|
||||
": currently contains \(lineCount)"
|
||||
|
||||
// Position violation at the start of the last line to avoid boundary issues
|
||||
let lastLine = file.lines.last
|
||||
let lastLineStartOffset = lastLine?.byteRange.location ?? 0
|
||||
let violationPosition = AbsolutePosition(utf8Offset: lastLineStartOffset.value)
|
||||
|
||||
let violation = ReasonedRuleViolation(
|
||||
position: violationPosition,
|
||||
reason: reason,
|
||||
severity: severity
|
||||
)
|
||||
violations.append(violation)
|
||||
}
|
||||
|
||||
var lineCount = file.lines.count
|
||||
let hasViolation = configuration.severityConfiguration.params.contains {
|
||||
$0.value < lineCount
|
||||
private func countNonCommentLines(in node: SourceFileSyntax) -> Int {
|
||||
var linesWithActualContent = Set<Int>()
|
||||
|
||||
for token in node.tokens(viewMode: .sourceAccurate) {
|
||||
addTokenContentLines(token, to: &linesWithActualContent)
|
||||
|
||||
// Process leading trivia
|
||||
addTriviaLines(token.leadingTrivia, startingAt: token.position, to: &linesWithActualContent)
|
||||
}
|
||||
return linesWithActualContent.count
|
||||
}
|
||||
|
||||
if hasViolation && configuration.ignoreCommentOnlyLines {
|
||||
lineCount = lineCountWithoutComments()
|
||||
private func addTokenContentLines(_ token: TokenSyntax, to lines: inout Set<Int>) {
|
||||
// Skip tokens whose text is empty or only whitespace
|
||||
// (e.g., EOF token, or an unlikely malformed token).
|
||||
guard !token.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||
|
||||
let startLocation = locationConverter.location(for: token.positionAfterSkippingLeadingTrivia)
|
||||
let endLocation = locationConverter.location(for: token.endPositionBeforeTrailingTrivia)
|
||||
|
||||
addLinesInRange(from: startLocation.line, to: endLocation.line, to: &lines)
|
||||
}
|
||||
|
||||
for parameter in configuration.severityConfiguration.params where lineCount > parameter.value {
|
||||
let reason = "File should contain \(configuration.severityConfiguration.warning) lines or less" +
|
||||
(configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") +
|
||||
": currently contains \(lineCount)"
|
||||
return [
|
||||
StyleViolation(
|
||||
ruleDescription: Self.description,
|
||||
severity: parameter.severity,
|
||||
location: Location(file: file.path, line: file.lines.count),
|
||||
reason: reason
|
||||
),
|
||||
]
|
||||
private func addTriviaLines(
|
||||
_ trivia: Trivia,
|
||||
startingAt startPosition: AbsolutePosition,
|
||||
to lines: inout Set<Int>
|
||||
) {
|
||||
var currentPosition = startPosition
|
||||
for piece in trivia {
|
||||
if !piece.isComment && !piece.isWhitespace {
|
||||
let startLocation = locationConverter.location(for: currentPosition)
|
||||
let endLocation = locationConverter.location(for: currentPosition + piece.sourceLength)
|
||||
addLinesInRange(from: startLocation.line, to: endLocation.line, to: &lines)
|
||||
}
|
||||
currentPosition += piece.sourceLength
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
private func addLinesInRange(from startLine: Int, to endLine: Int, to lines: inout Set<Int>) {
|
||||
guard startLine > 0 && startLine <= endLine else { return }
|
||||
for line in startLine...endLine {
|
||||
lines.insert(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user