Add a `summary` table reporter (#4820)
This commit is contained in:
parent
a840058cf5
commit
f2d15355be
1
BUILD
1
BUILD
|
@ -24,6 +24,7 @@ swift_library(
|
|||
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
|
||||
"@com_github_apple_swift_syntax//:optlibs",
|
||||
"@sourcekitten_com_github_jpsim_yams//:Yams",
|
||||
"@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable",
|
||||
] + select({
|
||||
"@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"],
|
||||
"//conditions:default": [],
|
||||
|
|
|
@ -111,6 +111,10 @@
|
|||
[Martin Redington](https://github.com/mildm8nnered)
|
||||
[#4248](https://github.com/realm/SwiftLint/issues/4248)
|
||||
|
||||
* Adds a new `summary` reporter, that displays the number of violations
|
||||
of each rule in a text table.
|
||||
[Martin Redington](https://github.com/mildm8nnered)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* Report violations in all `<scope>_length` rules when the error threshold is
|
||||
|
|
|
@ -61,7 +61,7 @@ let package = Package(
|
|||
),
|
||||
.target(
|
||||
name: "SwiftLintFramework",
|
||||
dependencies: frameworkDependencies
|
||||
dependencies: frameworkDependencies + ["SwiftyTextTable"]
|
||||
),
|
||||
.target(
|
||||
name: "SwiftLintTestHelpers",
|
||||
|
|
|
@ -49,6 +49,8 @@ public func reporterFrom(identifier: String) -> Reporter.Type { // swiftlint:dis
|
|||
return CodeClimateReporter.self
|
||||
case RelativePathReporter.identifier:
|
||||
return RelativePathReporter.self
|
||||
case SummaryReporter.identifier:
|
||||
return SummaryReporter.self
|
||||
default:
|
||||
queuedFatalError("no reporter with identifier '\(identifier)' available.")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
import Foundation
|
||||
import SwiftyTextTable
|
||||
|
||||
/// Reports a summary table of all violations
|
||||
public struct SummaryReporter: Reporter {
|
||||
// MARK: - Reporter Conformance
|
||||
|
||||
public static let identifier = "summary"
|
||||
public static let isRealtime = false
|
||||
|
||||
public var description: String {
|
||||
return "Reports a summary table of all violations."
|
||||
}
|
||||
|
||||
public static func generateReport(_ violations: [StyleViolation]) -> String {
|
||||
TextTable(violations: violations).renderWithExtraSeparator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftyTextTable
|
||||
|
||||
private extension TextTable {
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(violations: [StyleViolation]) {
|
||||
let numberOfWarningsHeader = "warnings"
|
||||
let numberOfErrorsHeader = "errors"
|
||||
let numberOfViolationsHeader = "total violations"
|
||||
let numberOfFilesHeader = "number of files"
|
||||
let columns = [
|
||||
TextTableColumn(header: "rule identifier"),
|
||||
TextTableColumn(header: "opt-in"),
|
||||
TextTableColumn(header: "correctable"),
|
||||
TextTableColumn(header: "custom"),
|
||||
TextTableColumn(header: numberOfWarningsHeader),
|
||||
TextTableColumn(header: numberOfErrorsHeader),
|
||||
TextTableColumn(header: numberOfViolationsHeader),
|
||||
TextTableColumn(header: numberOfFilesHeader)
|
||||
]
|
||||
self.init(columns: columns)
|
||||
|
||||
let ruleIdentifiersToViolationsMap = violations.group { $0.ruleIdentifier }
|
||||
let sortedRuleIdentifiers = ruleIdentifiersToViolationsMap.keys.sorted {
|
||||
let count1 = ruleIdentifiersToViolationsMap[$0]?.count ?? 0
|
||||
let count2 = ruleIdentifiersToViolationsMap[$1]?.count ?? 0
|
||||
if count1 > count2 {
|
||||
return true
|
||||
} else if count1 == count2 {
|
||||
return $0 < $1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var totalNumberOfWarnings = 0
|
||||
var totalNumberOfErrors = 0
|
||||
|
||||
for ruleIdentifier in sortedRuleIdentifiers {
|
||||
guard let ruleIdentifier = ruleIdentifiersToViolationsMap[ruleIdentifier]?.first?.ruleIdentifier else {
|
||||
continue
|
||||
}
|
||||
|
||||
let rule = primaryRuleList.list[ruleIdentifier]
|
||||
let violations = ruleIdentifiersToViolationsMap[ruleIdentifier]
|
||||
let numberOfWarnings = violations?.filter { $0.severity == .warning }.count ?? 0
|
||||
let numberOfErrors = violations?.filter { $0.severity == .error }.count ?? 0
|
||||
let numberOfViolations = numberOfWarnings + numberOfErrors
|
||||
totalNumberOfWarnings += numberOfWarnings
|
||||
totalNumberOfErrors += numberOfErrors
|
||||
let ruleViolations = ruleIdentifiersToViolationsMap[ruleIdentifier] ?? []
|
||||
let numberOfFiles = Set(ruleViolations.map { $0.location.file }).count
|
||||
|
||||
addRow(values: [
|
||||
ruleIdentifier,
|
||||
rule is OptInRule ? "yes" : "no",
|
||||
rule is CorrectableRule ? "yes" : "no",
|
||||
rule == nil ? "yes" : "no",
|
||||
numberOfWarnings.formattedString.leftPadded(forHeader: numberOfWarningsHeader),
|
||||
numberOfErrors.formattedString.leftPadded(forHeader: numberOfErrorsHeader),
|
||||
numberOfViolations.formattedString.leftPadded(forHeader: numberOfViolationsHeader),
|
||||
numberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader)
|
||||
])
|
||||
}
|
||||
|
||||
let totalNumberOfViolations = totalNumberOfWarnings + totalNumberOfErrors
|
||||
let totalNumberOfFiles = Set(violations.map { $0.location.file }).count
|
||||
addRow(values: [
|
||||
"Total",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
totalNumberOfWarnings.formattedString.leftPadded(forHeader: numberOfWarningsHeader),
|
||||
totalNumberOfErrors.formattedString.leftPadded(forHeader: numberOfErrorsHeader),
|
||||
totalNumberOfViolations.formattedString.leftPadded(forHeader: numberOfViolationsHeader),
|
||||
totalNumberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader)
|
||||
])
|
||||
}
|
||||
|
||||
func renderWithExtraSeparator() -> String {
|
||||
var output = render()
|
||||
var lines = output.components(separatedBy: "\n")
|
||||
if lines.count > 5, let lastLine = lines.last {
|
||||
lines.insert(lastLine, at: lines.count - 2)
|
||||
output = lines.joined(separator: "\n")
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
func leftPadded(forHeader header: String) -> String {
|
||||
let headerCount = header.count - self.count
|
||||
if headerCount > 0 {
|
||||
return String(repeating: " ", count: headerCount) + self
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
private extension Int {
|
||||
private static var numberFormatter: NumberFormatter = {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
return numberFormatter
|
||||
}()
|
||||
var formattedString: String {
|
||||
// swiftlint:disable:next legacy_objc_type
|
||||
Int.numberFormatter.string(from: NSNumber(value: self)) ?? ""
|
||||
}
|
||||
}
|
|
@ -6,19 +6,20 @@ import XCTest
|
|||
class ReporterTests: XCTestCase {
|
||||
func testReporterFromString() {
|
||||
let reporters: [Reporter.Type] = [
|
||||
XcodeReporter.self,
|
||||
JSONReporter.self,
|
||||
CSVReporter.self,
|
||||
CheckstyleReporter.self,
|
||||
CodeClimateReporter.self,
|
||||
JUnitReporter.self,
|
||||
HTMLReporter.self,
|
||||
CSVReporter.self,
|
||||
EmojiReporter.self,
|
||||
SonarQubeReporter.self,
|
||||
MarkdownReporter.self,
|
||||
GitHubActionsLoggingReporter.self,
|
||||
GitLabJUnitReporter.self,
|
||||
RelativePathReporter.self
|
||||
HTMLReporter.self,
|
||||
JSONReporter.self,
|
||||
JUnitReporter.self,
|
||||
MarkdownReporter.self,
|
||||
RelativePathReporter.self,
|
||||
SonarQubeReporter.self,
|
||||
SummaryReporter.self,
|
||||
XcodeReporter.self
|
||||
]
|
||||
for reporter in reporters {
|
||||
XCTAssertEqual(reporter.identifier, reporterFrom(identifier: reporter.identifier).identifier)
|
||||
|
@ -43,7 +44,7 @@ class ReporterTests: XCTestCase {
|
|||
severity: .error,
|
||||
location: location,
|
||||
reason: "Shorthand syntactic sugar should be used" +
|
||||
", i.e. [Int] instead of Array<Int>"),
|
||||
", i.e. [Int] instead of Array<Int>"),
|
||||
StyleViolation(ruleDescription: ColonRule.description,
|
||||
severity: .error,
|
||||
location: Location(file: nil),
|
||||
|
@ -155,4 +156,18 @@ class ReporterTests: XCTestCase {
|
|||
XCTAssertFalse(result.contains(absolutePath))
|
||||
XCTAssertTrue(result.contains(relativePath))
|
||||
}
|
||||
|
||||
func testSummaryReporter() {
|
||||
let expectedOutput = stringFromFile("CannedSummaryReporterOutput.txt")
|
||||
.trimmingTrailingCharacters(in: .whitespacesAndNewlines)
|
||||
let result = SummaryReporter.generateReport(generateViolations())
|
||||
XCTAssertEqual(result, expectedOutput)
|
||||
}
|
||||
|
||||
func testSummaryReporterWithNoViolations() {
|
||||
let expectedOutput = stringFromFile("CannedSummaryReporterNoViolationsOutput.txt")
|
||||
.trimmingTrailingCharacters(in: .whitespacesAndNewlines)
|
||||
let result = SummaryReporter.generateReport([])
|
||||
XCTAssertEqual(result, expectedOutput)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
||||
| rule identifier | opt-in | correctable | custom | warnings | errors | total violations | number of files |
|
||||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
||||
| Total | | | | 0 | 0 | 0 | 0 |
|
||||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
|
@ -0,0 +1,9 @@
|
|||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
||||
| rule identifier | opt-in | correctable | custom | warnings | errors | total violations | number of files |
|
||||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
||||
| line_length | no | no | no | 1 | 1 | 2 | 1 |
|
||||
| colon | no | no | no | 0 | 1 | 1 | 1 |
|
||||
| syntactic_sugar | no | no | no | 0 | 1 | 1 | 1 |
|
||||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
||||
| Total | | | | 1 | 3 | 4 | 2 |
|
||||
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
|
Loading…
Reference in New Issue