From f44aad0dbd824f6873375d80a1c6c55ee508097f Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 29 Oct 2023 04:03:54 -0700 Subject: [PATCH 1/5] Rewrite vertical_whitespace rule using SwiftSyntax --- .../Rules/Style/VerticalWhitespaceRule.swift | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 70751ab2a9..640ab1be51 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -1,9 +1,11 @@ import Foundation +import SwiftIDEUtils +import SwiftSyntax import SourceKittenFramework private let defaultDescriptionReason = "Limit vertical whitespace to a single empty line" -struct VerticalWhitespaceRule: CorrectableRule { +struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { var configuration = VerticalWhitespaceConfiguration() static let description = RuleDescription( @@ -67,14 +69,13 @@ struct VerticalWhitespaceRule: CorrectableRule { let blankLinesSections = extractSections(from: filteredLines) // filtering out violations in comments and strings - let stringAndComments = SyntaxKind.commentAndStringKinds - let syntaxMap = file.syntaxMap let result = blankLinesSections.compactMap { eachSection -> (lastLine: Line, linesToRemove: Int)? in guard let lastLine = eachSection.last else { return nil } - let kindInSection = syntaxMap.kinds(inByteRange: lastLine.byteRange) - if stringAndComments.isDisjoint(with: kindInSection) { + + let classificationsInRange = file.syntaxClassifications.classifications(inByteRange: lastLine.byteRange) + if !classificationsInRange.contains(where: \.isStringOrComment) { return (lastLine, eachSection.count) } @@ -148,3 +149,44 @@ struct VerticalWhitespaceRule: CorrectableRule { return [] } } + +private extension SyntaxClassification { + var isStringOrComment: Bool { + switch self { + case .docBlockComment, .docLineComment, .blockComment, .lineComment, .stringLiteral: + return true + case .attribute, .dollarIdentifier, .editorPlaceholder, .floatLiteral, .identifier, + .ifConfigDirective, .integerLiteral, .keyword, .none, .operator, .regexLiteral, .type: + return false + } + } +} + +private extension SyntaxClassifications { + func classifications(inByteRange byteRange: ByteRange) -> [SyntaxClassification] { + classificationsRanges(inByteRange: byteRange).map(\.kind) + } + + func classificationsRanges(inByteRange byteRange: ByteRange) -> [SyntaxClassifiedRange] { + func intersect(_ element: SyntaxClassifiedRange) -> Bool { + let otherRange = ByteSourceRange(offset: byteRange.location.value, length: byteRange.location.value) + return element.range.intersects(otherRange) + } + + func intersectsOrAfter(_ element: SyntaxClassifiedRange) -> Bool { + return element.offset + element.length > byteRange.location.value + } + + let array = Array(self) + guard let startIndex = array.firstIndexAssumingSorted(where: intersectsOrAfter) else { + return [] + } + + let tokensAfterFirstIntersection = array + .suffix(from: startIndex) + .prefix(while: { $0.offset < byteRange.upperBound.value }) + .filter(intersect) + + return Array(tokensAfterFirstIntersection) + } +} From 95c4407af7cd25a02514212928294bb518d39bb6 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 29 Oct 2023 04:37:34 -0700 Subject: [PATCH 2/5] use built-in impl --- .../Rules/Style/VerticalWhitespaceRule.swift | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 640ab1be51..7b1bdb9794 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -17,7 +17,27 @@ struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { Example("let abc = 0\n"), Example("let abc = 0\n\n"), Example("/* bcs \n\n\n\n*/"), - Example("// bca \n\n") + Example("// bca \n\n"), + Example(#""" + """ + This is a + + + + + very long + + + + string with newlines + + + + + + + """ + """#) ], triggeringExamples: [ Example("let aaaa = 0\n\n\n"), @@ -74,8 +94,10 @@ struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { return nil } - let classificationsInRange = file.syntaxClassifications.classifications(inByteRange: lastLine.byteRange) - if !classificationsInRange.contains(where: \.isStringOrComment) { + let range = ByteSourceRange(offset: lastLine.byteRange.location.value, + length: lastLine.byteRange.length.value) + let classificationsInRange = file.syntaxTree.classifications(in: range) + if !classificationsInRange.contains(where: \.kind.isStringOrComment) { return (lastLine, eachSection.count) } @@ -161,32 +183,3 @@ private extension SyntaxClassification { } } } - -private extension SyntaxClassifications { - func classifications(inByteRange byteRange: ByteRange) -> [SyntaxClassification] { - classificationsRanges(inByteRange: byteRange).map(\.kind) - } - - func classificationsRanges(inByteRange byteRange: ByteRange) -> [SyntaxClassifiedRange] { - func intersect(_ element: SyntaxClassifiedRange) -> Bool { - let otherRange = ByteSourceRange(offset: byteRange.location.value, length: byteRange.location.value) - return element.range.intersects(otherRange) - } - - func intersectsOrAfter(_ element: SyntaxClassifiedRange) -> Bool { - return element.offset + element.length > byteRange.location.value - } - - let array = Array(self) - guard let startIndex = array.firstIndexAssumingSorted(where: intersectsOrAfter) else { - return [] - } - - let tokensAfterFirstIntersection = array - .suffix(from: startIndex) - .prefix(while: { $0.offset < byteRange.upperBound.value }) - .filter(intersect) - - return Array(tokensAfterFirstIntersection) - } -} From 2a829f0e6ac691f178b246b31824fc2f0074df9d Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 29 Oct 2023 04:47:36 -0700 Subject: [PATCH 3/5] fix violation --- .../Rules/Style/VerticalWhitespaceRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 7b1bdb9794..3388785c46 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -1,7 +1,7 @@ import Foundation +import SourceKittenFramework import SwiftIDEUtils import SwiftSyntax -import SourceKittenFramework private let defaultDescriptionReason = "Limit vertical whitespace to a single empty line" From c3ca86480b1e0d46d6b952e902d62247942f48d8 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 29 Oct 2023 05:30:25 -0700 Subject: [PATCH 4/5] fix? --- .../Rules/Style/VerticalWhitespaceRule.swift | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 3388785c46..11014335ee 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -42,7 +42,14 @@ struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { triggeringExamples: [ Example("let aaaa = 0\n\n\n"), Example("struct AAAA {}\n\n\n\n"), - Example("class BBBB {}\n\n\n") + Example("class BBBB {}\n\n\n"), + Example(""" + func foo() {} + + + // MARK: - Something + + """) ], corrections: [ Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\nclass AAA {}\n"), @@ -90,18 +97,25 @@ struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { // filtering out violations in comments and strings let result = blankLinesSections.compactMap { eachSection -> (lastLine: Line, linesToRemove: Int)? in - guard let lastLine = eachSection.last else { + guard let firstLine = eachSection.first, let lastLine = eachSection.last else { return nil } - let range = ByteSourceRange(offset: lastLine.byteRange.location.value, - length: lastLine.byteRange.length.value) + let length = lastLine.byteRange.upperBound - firstLine.byteRange.lowerBound + let range = ByteSourceRange(offset: firstLine.byteRange.location.value, + length: length.value) let classificationsInRange = file.syntaxTree.classifications(in: range) - if !classificationsInRange.contains(where: \.kind.isStringOrComment) { - return (lastLine, eachSection.count) + .filter { element in + // "The provided classified ranges may extend beyond the provided `range`" + // means that we need to exclude elements that are only "touching" one + // of the limits of the range + return element.range.intersects(range) + } + if classificationsInRange.contains(where: \.kind.isStringOrComment) { + return nil } - return nil + return (lastLine, eachSection.count) } return result.filter { $0.linesToRemove >= configuration.maxEmptyLines } From eb422b00378f2b0740f6a280096113da4071992d Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 29 Oct 2023 06:12:53 -0700 Subject: [PATCH 5/5] a different approach --- .../Rules/Style/VerticalWhitespaceRule.swift | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 11014335ee..abff33d3bc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -37,6 +37,13 @@ struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { """ + """#), + Example(#""" + example(value: """ + Something + + + """) """#) ], triggeringExamples: [ @@ -97,25 +104,16 @@ struct VerticalWhitespaceRule: CorrectableRule, SourceKitFreeRule { // filtering out violations in comments and strings let result = blankLinesSections.compactMap { eachSection -> (lastLine: Line, linesToRemove: Int)? in - guard let firstLine = eachSection.first, let lastLine = eachSection.last else { + guard let lastLine = eachSection.last else { return nil } - let length = lastLine.byteRange.upperBound - firstLine.byteRange.lowerBound - let range = ByteSourceRange(offset: firstLine.byteRange.location.value, - length: length.value) - let classificationsInRange = file.syntaxTree.classifications(in: range) - .filter { element in - // "The provided classified ranges may extend beyond the provided `range`" - // means that we need to exclude elements that are only "touching" one - // of the limits of the range - return element.range.intersects(range) - } - if classificationsInRange.contains(where: \.kind.isStringOrComment) { - return nil + guard let classificationsInRange = file.syntaxTree.classification(at: lastLine.byteRange.location.value), + classificationsInRange.kind.isStringOrComment else { + return (lastLine, eachSection.count) } - return (lastLine, eachSection.count) + return nil } return result.filter { $0.linesToRemove >= configuration.maxEmptyLines }