From e676e1abbfadbfea4b2206a5f8e3a3f7007543e3 Mon Sep 17 00:00:00 2001 From: Ian Leitch Date: Sun, 9 Jun 2024 11:45:32 +0200 Subject: [PATCH] Add baseline capability (#751) --- CHANGELOG.md | 2 +- Sources/Frontend/Commands/ScanBehavior.swift | 19 +++++++++++- Sources/Frontend/Commands/ScanCommand.swift | 8 +++++ .../PeripheryKit/Indexer/SourceLocation.swift | 8 +++++ .../PeripheryKit/Indexer/SwiftIndexer.swift | 3 +- Sources/PeripheryKit/Results/Baseline.swift | 13 ++++++++ .../CheckstyleFormatter.swift | 0 .../CodeClimateFormatter.swift | 0 .../CsvFormatter.swift | 0 .../GitHubActionsFormatter.swift | 0 .../JsonFormatter.swift | 0 .../OutputDeclarationFilter.swift | 31 +++++++++++++++---- .../OutputFormatter.swift | 0 .../XcodeFormatter.swift | 0 Sources/PeripheryKit/ScanResult.swift | 4 +++ .../SourceGraph/SourceGraph.swift | 3 +- .../Syntax/UnusedParameterParser.swift | 6 ++-- Sources/Shared/Configuration.swift | 20 ++++++++++++ 18 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 Sources/PeripheryKit/Results/Baseline.swift rename Sources/PeripheryKit/{Formatters => Results}/CheckstyleFormatter.swift (100%) rename Sources/PeripheryKit/{Formatters => Results}/CodeClimateFormatter.swift (100%) rename Sources/PeripheryKit/{Formatters => Results}/CsvFormatter.swift (100%) rename Sources/PeripheryKit/{Formatters => Results}/GitHubActionsFormatter.swift (100%) rename Sources/PeripheryKit/{Formatters => Results}/JsonFormatter.swift (100%) rename Sources/PeripheryKit/{Formatters => Results}/OutputDeclarationFilter.swift (52%) rename Sources/PeripheryKit/{Formatters => Results}/OutputFormatter.swift (100%) rename Sources/PeripheryKit/{Formatters => Results}/XcodeFormatter.swift (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b88526322..d5181dbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ##### Enhancements -- None. +- Added baseline support. Write a baseline with `--write-baseline ` and use it with `--baseline `. ##### Bug Fixes diff --git a/Sources/Frontend/Commands/ScanBehavior.swift b/Sources/Frontend/Commands/ScanBehavior.swift index 806ed4355..d1c0b2524 100644 --- a/Sources/Frontend/Commands/ScanBehavior.swift +++ b/Sources/Frontend/Commands/ScanBehavior.swift @@ -61,13 +61,30 @@ final class ScanBehavior { do { results = try block(project) + let interval = logger.beginInterval("result:output") - let filteredResults = OutputDeclarationFilter().filter(results) + var baseline: Baseline? + + if let baselinePath = configuration.baseline { + let data = try Data(contentsOf: baselinePath.url) + baseline = try JSONDecoder().decode(Baseline.self, from: data) + } + + let filteredResults = try OutputDeclarationFilter().filter(results, with: baseline) if configuration.autoRemove { try ScanResultRemover().remove(results: filteredResults) } + if let baselinePath = configuration.writeBaseline { + let usrs = filteredResults + .flatMapSet { $0.usrs } + .union(baseline?.usrs ?? []) + let baseline = Baseline.v1(usrs: usrs.sorted()) + let data = try JSONEncoder().encode(baseline) + try data.write(to: baselinePath.url) + } + let output = try configuration.outputFormat.formatter.init(configuration: configuration).format(filteredResults) if configuration.outputFormat.supportsAuxiliaryOutput { diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index e90b90bb4..b009df607 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -123,6 +123,12 @@ struct ScanCommand: FrontendCommand { @Option(help: "JSON package manifest path (obtained using `swift package describe --type json` or manually)") var jsonPackageManifestPath: String? + @Option(help: "Baseline file path used to filter results") + var baseline: FilePath? + + @Option(help: "Baseline file path where results are written. Pass the same path to '--baseline' in subsequent scans to exclude the results recorded in the baseline.") + var writeBaseline: FilePath? + private static let defaultConfiguration = Configuration() func run() throws { @@ -174,6 +180,8 @@ struct ScanCommand: FrontendCommand { configuration.apply(\.$retainCodableProperties, retainCodableProperties) configuration.apply(\.$retainEncodableProperties, retainEncodableProperties) configuration.apply(\.$jsonPackageManifestPath, jsonPackageManifestPath) + configuration.apply(\.$baseline, baseline) + configuration.apply(\.$writeBaseline, writeBaseline) try scanBehavior.main { project in try Scan().perform(project: project) diff --git a/Sources/PeripheryKit/Indexer/SourceLocation.swift b/Sources/PeripheryKit/Indexer/SourceLocation.swift index 6af1a8eae..650073a40 100644 --- a/Sources/PeripheryKit/Indexer/SourceLocation.swift +++ b/Sources/PeripheryKit/Indexer/SourceLocation.swift @@ -1,4 +1,5 @@ import Foundation +import SystemPackage class SourceLocation { let file: SourceFile @@ -14,6 +15,13 @@ class SourceLocation { self.hashValueCache = [file.hashValue, line, column].hashValue } + func relativeTo(_ path: FilePath) -> SourceLocation { + let newPath = file.path.relativeTo(path) + let newFile = SourceFile(path: newPath, modules: file.modules) + newFile.importStatements = file.importStatements + return SourceLocation(file: newFile, line: line, column: column) + } + // MARK: - Private private func buildDescription(path: String) -> String { diff --git a/Sources/PeripheryKit/Indexer/SwiftIndexer.swift b/Sources/PeripheryKit/Indexer/SwiftIndexer.swift index 91f7e218e..710d747c6 100644 --- a/Sources/PeripheryKit/Indexer/SwiftIndexer.swift +++ b/Sources/PeripheryKit/Indexer/SwiftIndexer.swift @@ -501,8 +501,7 @@ public final class SwiftIndexer: Indexer { graph.withLock { for param in params { - let paramDecl = param.declaration - paramDecl.parent = functionDecl + let paramDecl = param.makeDeclaration(withParent: functionDecl) functionDecl.unusedParameters.insert(paramDecl) graph.addUnsafe(paramDecl) diff --git a/Sources/PeripheryKit/Results/Baseline.swift b/Sources/PeripheryKit/Results/Baseline.swift new file mode 100644 index 000000000..8a6fb5ae8 --- /dev/null +++ b/Sources/PeripheryKit/Results/Baseline.swift @@ -0,0 +1,13 @@ +import Foundation + +/// A baseline set of declarations that are excluded from results. +public enum Baseline: Codable { + case v1(usrs: [String]) + + public var usrs: Set { + switch self { + case .v1(let usrs): + return Set(usrs) + } + } +} diff --git a/Sources/PeripheryKit/Formatters/CheckstyleFormatter.swift b/Sources/PeripheryKit/Results/CheckstyleFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/CheckstyleFormatter.swift rename to Sources/PeripheryKit/Results/CheckstyleFormatter.swift diff --git a/Sources/PeripheryKit/Formatters/CodeClimateFormatter.swift b/Sources/PeripheryKit/Results/CodeClimateFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/CodeClimateFormatter.swift rename to Sources/PeripheryKit/Results/CodeClimateFormatter.swift diff --git a/Sources/PeripheryKit/Formatters/CsvFormatter.swift b/Sources/PeripheryKit/Results/CsvFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/CsvFormatter.swift rename to Sources/PeripheryKit/Results/CsvFormatter.swift diff --git a/Sources/PeripheryKit/Formatters/GitHubActionsFormatter.swift b/Sources/PeripheryKit/Results/GitHubActionsFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/GitHubActionsFormatter.swift rename to Sources/PeripheryKit/Results/GitHubActionsFormatter.swift diff --git a/Sources/PeripheryKit/Formatters/JsonFormatter.swift b/Sources/PeripheryKit/Results/JsonFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/JsonFormatter.swift rename to Sources/PeripheryKit/Results/JsonFormatter.swift diff --git a/Sources/PeripheryKit/Formatters/OutputDeclarationFilter.swift b/Sources/PeripheryKit/Results/OutputDeclarationFilter.swift similarity index 52% rename from Sources/PeripheryKit/Formatters/OutputDeclarationFilter.swift rename to Sources/PeripheryKit/Results/OutputDeclarationFilter.swift index d15fa5bd2..745a24ff7 100644 --- a/Sources/PeripheryKit/Formatters/OutputDeclarationFilter.swift +++ b/Sources/PeripheryKit/Results/OutputDeclarationFilter.swift @@ -5,25 +5,44 @@ import FilenameMatcher public final class OutputDeclarationFilter { private let configuration: Configuration - private let logger: ContextualLogger + private let logger: Logger + private let contextualLogger: ContextualLogger public required init(configuration: Configuration = .shared, logger: Logger = .init()) { self.configuration = configuration - self.logger = logger.contextualized(with: "report:filter") + self.logger = logger + self.contextualLogger = logger.contextualized(with: "report:filter") } - public func filter(_ declarations: [ScanResult]) -> [ScanResult] { + public func filter(_ declarations: [ScanResult], with baseline: Baseline?) throws -> [ScanResult] { + var declarations = declarations + + if let baseline { + var didFilterDeclaration = false + declarations = declarations.filter { + let isDisjoint = $0.usrs.isDisjoint(with: baseline.usrs) + if !isDisjoint { + didFilterDeclaration = true + } + return isDisjoint + } + + if !didFilterDeclaration { + logger.warn("No results were filtered by the baseline.") + } + } + if configuration.reportInclude.isEmpty && configuration.reportExclude.isEmpty { return declarations.sorted { $0.declaration < $1.declaration } } return declarations - .filter { + .filter { [contextualLogger] in let path = $0.declaration.location.file.path if configuration.reportIncludeMatchers.isEmpty { if configuration.reportExcludeMatchers.anyMatch(filename: path.string) { - self.logger.debug("Excluding \(path.string)") + contextualLogger.debug("Excluding \(path.string)") return false } @@ -31,7 +50,7 @@ public final class OutputDeclarationFilter { } if configuration.reportIncludeMatchers.anyMatch(filename: path.string) { - self.logger.debug("Including \(path.string)") + contextualLogger.debug("Including \(path.string)") return true } diff --git a/Sources/PeripheryKit/Formatters/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/OutputFormatter.swift rename to Sources/PeripheryKit/Results/OutputFormatter.swift diff --git a/Sources/PeripheryKit/Formatters/XcodeFormatter.swift b/Sources/PeripheryKit/Results/XcodeFormatter.swift similarity index 100% rename from Sources/PeripheryKit/Formatters/XcodeFormatter.swift rename to Sources/PeripheryKit/Results/XcodeFormatter.swift diff --git a/Sources/PeripheryKit/ScanResult.swift b/Sources/PeripheryKit/ScanResult.swift index 00dfca5d8..19c945429 100644 --- a/Sources/PeripheryKit/ScanResult.swift +++ b/Sources/PeripheryKit/ScanResult.swift @@ -10,4 +10,8 @@ public struct ScanResult { let declaration: Declaration let annotation: Annotation + + public var usrs: Set { + declaration.usrs + } } diff --git a/Sources/PeripheryKit/SourceGraph/SourceGraph.swift b/Sources/PeripheryKit/SourceGraph/SourceGraph.swift index 6d1313f4f..f7911d239 100644 --- a/Sources/PeripheryKit/SourceGraph/SourceGraph.swift +++ b/Sources/PeripheryKit/SourceGraph/SourceGraph.swift @@ -260,7 +260,8 @@ public final class SourceGraph { func markUnusedModuleImport(_ statement: ImportStatement) { withLock { - let usr = "\(statement.location.description)-\(statement.module)" + let location = statement.location.relativeTo(.current) + let usr = "import-\(statement.module)-\(location)" let decl = Declaration(kind: .module, usrs: [usr], location: statement.location) decl.name = statement.module unusedModuleImports.insert(decl) diff --git a/Sources/PeripheryKit/Syntax/UnusedParameterParser.swift b/Sources/PeripheryKit/Syntax/UnusedParameterParser.swift index 8973da668..b9aba610a 100644 --- a/Sources/PeripheryKit/Syntax/UnusedParameterParser.swift +++ b/Sources/PeripheryKit/Syntax/UnusedParameterParser.swift @@ -63,11 +63,13 @@ final class Parameter: Item, Hashable { return secondName ?? firstName ?? "" } - var declaration: Declaration { + func makeDeclaration(withParent parent: Declaration) -> Declaration { let functionName = function?.fullName ?? "func()" - let usr = "\(functionName)-\(name)-\(location)" + let parentUsrs = parent.usrs.joined(separator: "-") + let usr = "param-\(name)-\(functionName)-\(parentUsrs)" let decl = Declaration(kind: .varParameter, usrs: [usr], location: location) decl.name = name + decl.parent = parent return decl } diff --git a/Sources/Shared/Configuration.swift b/Sources/Shared/Configuration.swift index 9204b2b91..09744a1f4 100644 --- a/Sources/Shared/Configuration.swift +++ b/Sources/Shared/Configuration.swift @@ -122,6 +122,12 @@ public final class Configuration { @Setting(key: "json_package_manifest_path", defaultValue: nil) public var jsonPackageManifestPath: String? + @Setting(key: "baseline", defaultValue: nil) + public var baseline: FilePath? + + @Setting(key: "write_baseline", defaultValue: nil) + public var writeBaseline: FilePath? + // Non user facing. public var guidedSetup: Bool = false public var removalOutputBasePath: FilePath? @@ -280,6 +286,14 @@ public final class Configuration { config[$jsonPackageManifestPath.key] = jsonPackageManifestPath } + if $baseline.hasNonDefaultValue { + config[$baseline.key] = baseline + } + + if $writeBaseline.hasNonDefaultValue { + config[$writeBaseline.key] = writeBaseline + } + return try Yams.dump(object: config) } @@ -373,6 +387,10 @@ public final class Configuration { $retainEncodableProperties.assign(value) case $jsonPackageManifestPath.key: $jsonPackageManifestPath.assign(value) + case $baseline.key: + $baseline.assign(value) + case $writeBaseline.key: + $writeBaseline.assign(value) default: logger.warn("\(path.string): invalid key '\(key)'") } @@ -417,6 +435,8 @@ public final class Configuration { $retainCodableProperties.reset() $retainEncodableProperties.reset() $jsonPackageManifestPath.reset() + $baseline.reset() + $writeBaseline.reset() } // MARK: - Helpers