Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var targets: [PackageDescription.Target] = [
.product(name: "Yams", package: "Yams"),
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "FilenameMatcher", package: "swift-filename-matcher"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.target(
Expand Down
4 changes: 4 additions & 0 deletions Sources/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ public final class Configuration {
@Setting(key: "bazel_check_visibility", defaultValue: false)
public var bazelCheckVisibility: Bool

@Setting(key: "sonarqube_rule_severity", defaultValue: .default, setter: { SonarQubeRuleSeverity(anyValue: $0) })
public var sonarqubeRuleSeverity: SonarQubeRuleSeverity

// Non user facing.
public var guidedSetup: Bool = false
public var projectRoot: FilePath = .init()
Expand Down Expand Up @@ -225,6 +228,7 @@ public final class Configuration {
$skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults,
$jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline,
$writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelIndexStore, $bazelCheckVisibility,
$sonarqubeRuleSeverity,
]

private func buildFilenameMatchers(with patterns: [String]) -> [FilenameMatcher] {
Expand Down
1 change: 1 addition & 0 deletions Sources/Configuration/OutputFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum OutputFormat: String, CaseIterable {
case githubActions = "github-actions"
case githubMarkdown = "github-markdown"
case gitlabCodeQuality = "gitlab-codequality"
case sonarQube = "sonarqube"

public static let `default` = OutputFormat.xcode

Expand Down
21 changes: 21 additions & 0 deletions Sources/Configuration/SonarQubeRuleSeverity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import ArgumentParser

public enum SonarQubeRuleSeverity: String, CaseIterable, ExpressibleByArgument {
case blocker = "BLOCKER"
case critical = "CRITICAL"
case major = "MAJOR"
case minor = "MINOR"
case info = "INFO"

public static let `default`: Self = .info

init?(anyValue: Any) {
if let option = anyValue as? Self {
self = option
return
}
guard let stringValue = anyValue as? String else { return nil }

self.init(rawValue: stringValue)
}
}
4 changes: 4 additions & 0 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ struct ScanCommand: ParsableCommand {
@Flag(help: "Enable Bazel visibility checking")
var bazelCheckVisibility: Bool = defaultConfiguration.$bazelCheckVisibility.defaultValue

@Option(help: "SonarQube Rule severity")
var sonarqubeRuleSeverity: SonarQubeRuleSeverity = .default

private static let defaultConfiguration = Configuration()

func run() throws {
Expand Down Expand Up @@ -223,6 +226,7 @@ struct ScanCommand: ParsableCommand {
configuration.apply(\.$bazelFilter, bazelFilter)
configuration.apply(\.$bazelIndexStore, bazelIndexStore)
configuration.apply(\.$bazelCheckVisibility, bazelCheckVisibility)
configuration.apply(\.$sonarqubeRuleSeverity, sonarqubeRuleSeverity)

configuration.buildFilenameMatchers()

Expand Down
2 changes: 2 additions & 0 deletions Sources/PeripheryKit/Results/OutputFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ public extension OutputFormat {
GitHubMarkdownFormatter.self
case .gitlabCodeQuality:
GitLabCodeQualityFormatter.self
case .sonarQube:
SonarQubeFormatter.self
}
}
}
107 changes: 107 additions & 0 deletions Sources/PeripheryKit/Results/SonarQubeFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Configuration
import Foundation
import Logger
import SystemPackage

final class SonarQubeFormatter: OutputFormatter {
let configuration: Configuration

let logger: Logger

lazy var currentFilePath: FilePath = .current

init(configuration: Configuration, logger: Logger) {
self.logger = logger
self.configuration = configuration
}

func format(_ results: [ScanResult], colored: Bool) throws -> String? {
var ruleIds: Set<String> = []
var rules: [Any] = []
var issues: [Any] = []

for result in results {
let ruleId = describe(result.annotation)
if !ruleIds.contains(ruleId) {
ruleIds.insert(ruleId)
rules.append([
"id": ruleId,
"name": result.annotation.name,
"description": result.annotation.description,
"engineId": "periphery",
"cleanCodeAttribute": "EFFICIENT",
"type": "CODE_SMELL",
"severity": configuration.sonarqubeRuleSeverity.rawValue
])
}

var descriptions = self.describe(result, colored: false)
guard !descriptions.isEmpty else { continue }
let primaryResult = descriptions.removeFirst()
let location = primaryResult.0
var issue: [String: Any] = [
"ruleId": ruleId,
"primaryLocation": [
"message": primaryResult.1,
"filePath": outputPath(location).string,
"textRange": [
"startLine": location.line,
"startColumn": max(0, location.column - 1)
]
]
]
if !descriptions.isEmpty {
issue["secondaryLocations"] = descriptions.map({ location, message in
[
"message": message,
"filePath": outputPath(location).string,
"textRange": [
"startLine": location.line,
"startColumn": max(0, location.column - 1)
]
]
})
}
issues.append(issue)
}

let report = [
"rules": rules,
"issues": issues
]
let data = try JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .withoutEscapingSlashes])
return String(bytes: data, encoding: .utf8)
}
}

private extension ScanResult.Annotation {
var name: String {
switch self {
case .unused:
"Unused Declaration"
case .assignOnlyProperty:
"Assign-only Property"
case .redundantProtocol:
"Redundant Protocol"
case .redundantPublicAccessibility:
"Redundant Public Accessibility"
case .superfluousIgnoreCommand:
"Superfluous Ignore Comment"
}
}

var description: String {
switch self {
case .unused:
"Unused Declaration"
case .assignOnlyProperty:
"Property is assigned but never used"
case .redundantProtocol:
"Protocol is never used as an existential type"
case .redundantPublicAccessibility:
"Public accessibility is redundant as declaration is not used outside its module"
case .superfluousIgnoreCommand:
"Ignore comment is unnecessary as declaration is referenced"
}
}
}