Files
SwiftLint/Source/SwiftLintFramework/Rules/VerticalWhitespaceRule.swift
T
JP Simard b83e0991b9 Remove all file headers
The MIT license doesn't require that all files be prepended with this
licensing or copyright information. Realm confirmed that they're ok with this
change. This will enable some companies to contribute to SwiftLint and the
date & authorship information will remain accessible via git source control.
2018-05-04 13:42:02 -07:00

155 lines
5.6 KiB
Swift

import Foundation
import SourceKittenFramework
private let defaultDescriptionReason = "Limit vertical whitespace to a single empty line."
public struct VerticalWhitespaceRule: CorrectableRule, ConfigurationProviderRule {
public var configuration = VerticalWhitespaceConfiguration(maxEmptyLines: 1)
public init() {}
public static let description = RuleDescription(
identifier: "vertical_whitespace",
name: "Vertical Whitespace",
description: defaultDescriptionReason,
kind: .style,
nonTriggeringExamples: [
"let abc = 0\n",
"let abc = 0\n\n",
"/* bcs \n\n\n\n*/",
"// bca \n\n"
],
triggeringExamples: [
"let aaaa = 0\n\n\n",
"struct AAAA {}\n\n\n\n",
"class BBBB {}\n\n\n"
],
corrections: [
"let b = 0\n\n\nclass AAA {}\n": "let b = 0\n\nclass AAA {}\n",
"let c = 0\n\n\nlet num = 1\n": "let c = 0\n\nlet num = 1\n",
"// bca \n\n\n": "// bca \n\n"
] // End of line autocorrections are handled by Trailing Newline Rule.
)
private var configuredDescriptionReason: String {
guard configuration.maxEmptyLines == 1 else {
return "Limit vertical whitespace to maximum \(configuration.maxEmptyLines) empty lines."
}
return defaultDescriptionReason
}
public func validate(file: File) -> [StyleViolation] {
let linesSections = violatingLineSections(in: file)
guard !linesSections.isEmpty else {
return []
}
return linesSections.map { eachLastLine, eachSectionCount in
return StyleViolation(
ruleDescription: type(of: self).description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file.path, line: eachLastLine.index),
reason: configuredDescriptionReason + " Currently \(eachSectionCount + 1)."
)
}
}
private typealias LineSection = (lastLine: Line, linesToRemove: Int)
private func violatingLineSections(in file: File) -> [LineSection] {
let nonSpaceRegex = regex("\\S", options: [])
let filteredLines = file.lines.filter {
nonSpaceRegex.firstMatch(in: file.contents, options: [], range: $0.range) == nil
}
guard !filteredLines.isEmpty else {
return []
}
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) {
return (lastLine, eachSection.count)
}
return nil
}
return result.filter { $0.linesToRemove >= configuration.maxEmptyLines }
}
private func extractSections(from lines: [Line]) -> [[Line]] {
var blankLinesSections = [[Line]]()
var lineSection = [Line]()
var previousIndex = 0
for (index, line) in lines.enumerated() {
let previousLine: Line = lines[previousIndex]
if previousLine.index + 1 == line.index {
lineSection.append(line)
} else if !lineSection.isEmpty {
blankLinesSections.append(lineSection)
lineSection.removeAll()
}
previousIndex = index
}
if !lineSection.isEmpty {
blankLinesSections.append(lineSection)
}
return blankLinesSections
}
public func correct(file: File) -> [Correction] {
let linesSections = violatingLineSections(in: file)
if linesSections.isEmpty { return [] }
var indexOfLinesToDelete = [Int]()
for section in linesSections {
let linesToRemove = section.linesToRemove - configuration.maxEmptyLines + 1
let start = section.lastLine.index - linesToRemove
indexOfLinesToDelete.append(contentsOf: start..<section.lastLine.index)
}
var correctedLines = [String]()
var corrections = [Correction]()
for currentLine in file.lines {
// Doesn't correct lines where rule is disabled
if file.ruleEnabled(violatingRanges: [currentLine.range], for: self).isEmpty {
correctedLines.append(currentLine.content)
continue
}
// removes lines by skipping them from correctedLines
if Set(indexOfLinesToDelete).contains(currentLine.index) {
let description = type(of: self).description
let location = Location(file: file, characterOffset: currentLine.range.location)
//reports every line that is being deleted
corrections.append(Correction(ruleDescription: description, location: location))
continue // skips line
}
// all lines that pass get added to final output file
correctedLines.append(currentLine.content)
}
// converts lines back to file and adds trailing line
if !corrections.isEmpty {
file.write(correctedLines.joined(separator: "\n") + "\n")
return corrections
}
return []
}
}