mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Add ignores_literals option to trailing_whitespace rule (#6309)
This commit is contained in:
committed by
GitHub
parent
67e98911fc
commit
2c2ca671ed
@@ -95,6 +95,11 @@
|
||||
[nandhinisubbu](https://github.com/nandhinisubbu)
|
||||
[#6304](https://github.com/realm/SwiftLint/issues/6304)
|
||||
|
||||
* Add `ignores_literals` configuration for `trailing_whitespace` rule.
|
||||
It allows to ignore trailing whitespace in multiline strings.
|
||||
[nandhinisubbu](https://github.com/nandhinisubbu)
|
||||
[#6194](https://github.com/realm/SwiftLint/issues/6194)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ignore function, initializer and subscript declarations alike when the
|
||||
|
||||
@@ -198,29 +198,6 @@ private final class InterpolatedStringLineVisitor: SyntaxVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
// Visitor to find line ranges covered by multiline string literals
|
||||
private final class MultilineStringLiteralVisitor: SyntaxVisitor {
|
||||
let locationConverter: SourceLocationConverter
|
||||
var linesSpanned = Set<Int>()
|
||||
|
||||
init(locationConverter: SourceLocationConverter) {
|
||||
self.locationConverter = locationConverter
|
||||
super.init(viewMode: .sourceAccurate)
|
||||
}
|
||||
|
||||
override func visitPost(_ node: StringLiteralExprSyntax) {
|
||||
guard node.openingQuote.tokenKind == .multilineStringQuote else {
|
||||
return
|
||||
}
|
||||
let startLocation = locationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
|
||||
let endLocation = locationConverter.location(for: node.endPositionBeforeTrailingTrivia)
|
||||
guard startLocation.line < endLocation.line else {
|
||||
return
|
||||
}
|
||||
linesSpanned.formUnion(startLocation.line...endLocation.line)
|
||||
}
|
||||
}
|
||||
|
||||
// Visitor to find lines with regex literals
|
||||
private final class RegexLiteralVisitor: SyntaxVisitor {
|
||||
let locationConverter: SourceLocationConverter
|
||||
|
||||
+2
@@ -10,4 +10,6 @@ struct TrailingWhitespaceConfiguration: SeverityBasedRuleConfiguration {
|
||||
private(set) var ignoresEmptyLines = false
|
||||
@ConfigurationElement(key: "ignores_comments")
|
||||
private(set) var ignoresComments = true
|
||||
@ConfigurationElement(key: "ignores_literals")
|
||||
private(set) var ignoresLiterals = false
|
||||
}
|
||||
|
||||
@@ -15,9 +15,14 @@ struct TrailingWhitespaceRule: Rule {
|
||||
nonTriggeringExamples: [
|
||||
Example("let name: String\n"), Example("//\n"), Example("// \n"),
|
||||
Example("let name: String //\n"), Example("let name: String // \n"),
|
||||
Example("let stringWithSpace = \"hello \"\n"),
|
||||
Example("let multiline = \"\"\"\n line with spaces \n \"\"\" \n",
|
||||
configuration: ["ignores_literals": true]),
|
||||
],
|
||||
triggeringExamples: [
|
||||
Example("let name: String↓ \n"), Example("/* */ let name: String↓ \n")
|
||||
Example("let name: String↓ \n"), Example("/* */ let name: String↓ \n"),
|
||||
Example("let codeWithSpace = 123↓ \n", configuration: ["ignores_literals": true],
|
||||
testWrappingInComment: false),
|
||||
],
|
||||
corrections: [
|
||||
Example("let name: String↓ \n"): Example("let name: String\n"),
|
||||
@@ -32,12 +37,20 @@ private extension TrailingWhitespaceRule {
|
||||
private var linesFullyCoveredByBlockComments = Set<Int>()
|
||||
private var linesEndingWithComment = Set<Int>()
|
||||
|
||||
// Pre-computed string literal information for performance
|
||||
private var stringLiteralLines = Set<Int>()
|
||||
|
||||
override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
|
||||
// Pre-compute all comment information in a single pass if needed
|
||||
if configuration.ignoresComments {
|
||||
precomputeCommentInformation(node)
|
||||
}
|
||||
|
||||
// Pre-compute string literal information if needed
|
||||
if configuration.ignoresLiterals {
|
||||
precomputeStringLiteralInformation(node)
|
||||
}
|
||||
|
||||
// Process each line for trailing whitespace violations
|
||||
for lineContents in file.lines {
|
||||
let line = lineContents.content
|
||||
@@ -66,6 +79,14 @@ private extension TrailingWhitespaceRule {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply `ignoresLiterals` configuration
|
||||
if configuration.ignoresLiterals {
|
||||
// Check if line contains string literals
|
||||
if stringLiteralLines.contains(lineNumber) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate violation position
|
||||
let lineStartPos = locationConverter.position(ofLine: lineNumber, column: 1)
|
||||
let violationStartOffset = line.utf8.count - trailingWhitespaceInfo.byteLength
|
||||
@@ -91,6 +112,13 @@ private extension TrailingWhitespaceRule {
|
||||
determineLineEndingComments(using: lineCommentRanges)
|
||||
}
|
||||
|
||||
/// Pre-computes string literal information in a single pass for better performance
|
||||
private func precomputeStringLiteralInformation(_ node: SourceFileSyntax) {
|
||||
// Collects line numbers that contain multiline string literals
|
||||
let stringLiteralVisitor = MultilineStringLiteralVisitor(locationConverter: locationConverter)
|
||||
stringLiteralLines = stringLiteralVisitor.walk(tree: node, handler: \.linesSpanned)
|
||||
}
|
||||
|
||||
/// Collects ranges of line comments organized by line number
|
||||
private func collectLineCommentRanges(from node: SourceFileSyntax) -> [Int: [Range<AbsolutePosition>]] {
|
||||
var lineCommentRanges: [Int: [Range<AbsolutePosition>]] = [:]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import SwiftSyntax
|
||||
|
||||
/// Visitor to collect line numbers that are covered by multiline string literals.
|
||||
///
|
||||
/// This visitor traverses the syntax tree to identify multiline string literals (those using triple quotes `"""`)
|
||||
/// and collects all line numbers that fall within their boundaries. This is useful for rules that need to
|
||||
/// apply different behavior to content inside multiline string literals.
|
||||
public final class MultilineStringLiteralVisitor: SyntaxVisitor {
|
||||
/// The location converter to use for mapping positions to line numbers.
|
||||
private let locationConverter: SourceLocationConverter
|
||||
|
||||
/// Line numbers that are covered by multiline string literals.
|
||||
public private(set) var linesSpanned = Set<Int>()
|
||||
|
||||
/// Initializer.
|
||||
///
|
||||
/// - Parameter locationConverter: The location converter to use for mapping positions to line numbers.
|
||||
public init(locationConverter: SourceLocationConverter) {
|
||||
self.locationConverter = locationConverter
|
||||
super.init(viewMode: .sourceAccurate)
|
||||
}
|
||||
|
||||
/// Visits string literal expressions and collects line numbers for multiline string literals.
|
||||
///
|
||||
/// Only processes string literals that use triple quotes (`"""`) and span multiple lines.
|
||||
/// Single-line string literals are ignored.
|
||||
///
|
||||
/// - Parameter node: The string literal expression to examine.
|
||||
override public func visitPost(_ node: StringLiteralExprSyntax) {
|
||||
guard node.openingQuote.tokenKind == .multilineStringQuote else {
|
||||
return
|
||||
}
|
||||
let startLocation = locationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
|
||||
let endLocation = locationConverter.location(for: node.endPositionBeforeTrailingTrivia)
|
||||
guard startLocation.line < endLocation.line else {
|
||||
return
|
||||
}
|
||||
linesSpanned.formUnion(startLocation.line...endLocation.line)
|
||||
}
|
||||
}
|
||||
@@ -29,4 +29,22 @@ final class TrailingWhitespaceRuleTests: SwiftLintTestCase {
|
||||
ruleConfiguration: ["ignores_empty_lines": false, "ignores_comments": false],
|
||||
commentDoesntViolate: false)
|
||||
}
|
||||
|
||||
func testWithIgnoresLiteralsEnabled() {
|
||||
// Perform additional tests with the ignores_literals setting enabled.
|
||||
// This setting only ignores trailing whitespace inside multiline string literals.
|
||||
let baseDescription = TrailingWhitespaceRule.description
|
||||
let nonTriggeringExamples = baseDescription.nonTriggeringExamples + [
|
||||
Example("let multiline = \"\"\"\n content \n \"\"\"\n"),
|
||||
]
|
||||
let triggeringExamples = baseDescription.triggeringExamples + [
|
||||
Example("let codeWithSpace = 123 \n"),
|
||||
Example("var number = 42 \n"),
|
||||
]
|
||||
let description = baseDescription.with(nonTriggeringExamples: nonTriggeringExamples)
|
||||
.with(triggeringExamples: triggeringExamples)
|
||||
|
||||
verifyRule(description,
|
||||
ruleConfiguration: ["ignores_literals": true])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,8 @@ final class RuleConfigurationTests: SwiftLintTestCase {
|
||||
func testTrailingWhitespaceConfigurationThrowsOnBadConfig() {
|
||||
let config = "unknown"
|
||||
var configuration = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
checkError(Issue.invalidConfiguration(ruleID: TrailingWhitespaceRule.identifier)) {
|
||||
try configuration.apply(configuration: config)
|
||||
}
|
||||
@@ -145,27 +146,32 @@ final class RuleConfigurationTests: SwiftLintTestCase {
|
||||
|
||||
func testTrailingWhitespaceConfigurationInitializerSetsIgnoresEmptyLines() {
|
||||
let configuration1 = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
XCTAssertFalse(configuration1.ignoresEmptyLines)
|
||||
|
||||
let configuration2 = TrailingWhitespaceConfiguration(ignoresEmptyLines: true,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
XCTAssertTrue(configuration2.ignoresEmptyLines)
|
||||
}
|
||||
|
||||
func testTrailingWhitespaceConfigurationInitializerSetsIgnoresComments() {
|
||||
let configuration1 = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
XCTAssertTrue(configuration1.ignoresComments)
|
||||
|
||||
let configuration2 = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: false)
|
||||
ignoresComments: false,
|
||||
ignoresLiterals: false)
|
||||
XCTAssertFalse(configuration2.ignoresComments)
|
||||
}
|
||||
|
||||
func testTrailingWhitespaceConfigurationApplyConfigurationSetsIgnoresEmptyLines() {
|
||||
var configuration = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
do {
|
||||
let config1 = ["ignores_empty_lines": true]
|
||||
try configuration.apply(configuration: config1)
|
||||
@@ -181,7 +187,8 @@ final class RuleConfigurationTests: SwiftLintTestCase {
|
||||
|
||||
func testTrailingWhitespaceConfigurationApplyConfigurationSetsIgnoresComments() {
|
||||
var configuration = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
do {
|
||||
let config1 = ["ignores_comments": true]
|
||||
try configuration.apply(configuration: config1)
|
||||
@@ -197,22 +204,27 @@ final class RuleConfigurationTests: SwiftLintTestCase {
|
||||
|
||||
func testTrailingWhitespaceConfigurationCompares() {
|
||||
let configuration1 = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
let configuration2 = TrailingWhitespaceConfiguration(ignoresEmptyLines: true,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
XCTAssertNotEqual(configuration1, configuration2)
|
||||
|
||||
let configuration3 = TrailingWhitespaceConfiguration(ignoresEmptyLines: true,
|
||||
ignoresComments: true)
|
||||
ignoresComments: true,
|
||||
ignoresLiterals: false)
|
||||
XCTAssertEqual(configuration2, configuration3)
|
||||
|
||||
let configuration4 = TrailingWhitespaceConfiguration(ignoresEmptyLines: false,
|
||||
ignoresComments: false)
|
||||
ignoresComments: false,
|
||||
ignoresLiterals: false)
|
||||
|
||||
XCTAssertNotEqual(configuration1, configuration4)
|
||||
|
||||
let configuration5 = TrailingWhitespaceConfiguration(ignoresEmptyLines: true,
|
||||
ignoresComments: false)
|
||||
ignoresComments: false,
|
||||
ignoresLiterals: false)
|
||||
|
||||
XCTAssertNotEqual(configuration1, configuration5)
|
||||
}
|
||||
|
||||
@@ -1207,6 +1207,7 @@ trailing_whitespace:
|
||||
severity: warning
|
||||
ignores_empty_lines: false
|
||||
ignores_comments: true
|
||||
ignores_literals: false
|
||||
meta:
|
||||
opt-in: false
|
||||
correctable: true
|
||||
|
||||
Reference in New Issue
Block a user