Add a `summary` table reporter (#4820)

This commit is contained in:
Martin Redington 2023-03-25 07:28:42 +00:00 committed by GitHub
parent a840058cf5
commit f2d15355be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 10 deletions

1
BUILD
View File

@ -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": [],

View File

@ -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

View File

@ -61,7 +61,7 @@ let package = Package(
),
.target(
name: "SwiftLintFramework",
dependencies: frameworkDependencies
dependencies: frameworkDependencies + ["SwiftyTextTable"]
),
.target(
name: "SwiftLintTestHelpers",

View File

@ -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.")
}

View File

@ -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)) ?? ""
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,5 @@
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
| rule identifier | opt-in | correctable | custom | warnings | errors | total violations | number of files |
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+
| Total | | | | 0 | 0 | 0 | 0 |
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+

View File

@ -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 |
+-----------------+--------+-------------+--------+----------+--------+------------------+-----------------+