From a606a4edb65104cb52b2b9f44530e062f9c4009e Mon Sep 17 00:00:00 2001 From: Wiley Kestner Date: Thu, 3 Aug 2023 18:36:13 +0200 Subject: [PATCH] Add optional JSON output (#52) - Allow users to select a custom reporter that prints JSON instead of `sed` commands - Opt-in by adding `"reporter": "json"` to the configuration file --- Sources/unused-imports/BUILD | 5 +++ .../Reporters/JSONReporter.swift | 11 +++++ .../Reporters/SedCommandReporter.swift | 10 +++++ .../Reporters/UnusedImportReporter.swift | 3 ++ .../SourceFileWithUnusedImports.swift | 8 ++++ .../UnusedImportStatement.swift | 8 ++++ Sources/unused-imports/main.swift | 41 +++++++++++++++++-- 7 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 Sources/unused-imports/Reporters/JSONReporter.swift create mode 100644 Sources/unused-imports/Reporters/SedCommandReporter.swift create mode 100644 Sources/unused-imports/Reporters/UnusedImportReporter.swift create mode 100644 Sources/unused-imports/SourceFileWithUnusedImports.swift create mode 100644 Sources/unused-imports/UnusedImportStatement.swift diff --git a/Sources/unused-imports/BUILD b/Sources/unused-imports/BUILD index 96716e3..72a9070 100644 --- a/Sources/unused-imports/BUILD +++ b/Sources/unused-imports/BUILD @@ -4,6 +4,11 @@ swift_binary( name = "unused-imports", srcs = [ "main.swift", + "SourceFileWithUnusedImports.swift", + "UnusedImportStatement.swift", + "Reporters/JSONReporter.swift", + "Reporters/SedCommandReporter.swift", + "Reporters/UnusedImportReporter.swift", ], tags = [ "manual", diff --git a/Sources/unused-imports/Reporters/JSONReporter.swift b/Sources/unused-imports/Reporters/JSONReporter.swift new file mode 100644 index 0000000..1d2328b --- /dev/null +++ b/Sources/unused-imports/Reporters/JSONReporter.swift @@ -0,0 +1,11 @@ +import Foundation + +struct JSONReporter: UnusedImportReporter { + func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) { + let jsonEncoder = JSONEncoder() + let removableImportsJSONData = try! jsonEncoder.encode(sourceFilesWithUnusedImports) + let removableImportsJSONString = String(data: removableImportsJSONData, encoding: String.Encoding.utf8)! + + print(removableImportsJSONString) + } +} diff --git a/Sources/unused-imports/Reporters/SedCommandReporter.swift b/Sources/unused-imports/Reporters/SedCommandReporter.swift new file mode 100644 index 0000000..c166c32 --- /dev/null +++ b/Sources/unused-imports/Reporters/SedCommandReporter.swift @@ -0,0 +1,10 @@ +import Foundation + +struct SedCommandReporter: UnusedImportReporter { + func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) { + for sourceFile in sourceFilesWithUnusedImports.sorted() { + let sedCmd = sourceFile.unusedImportStatements.map { unusedImport in "\(unusedImport.lineNumber)d" }.joined(separator: ";") + print("/usr/bin/sed -i \"\" '\(sedCmd)' '\(sourceFile.path)'") + } + } +} diff --git a/Sources/unused-imports/Reporters/UnusedImportReporter.swift b/Sources/unused-imports/Reporters/UnusedImportReporter.swift new file mode 100644 index 0000000..5da16dc --- /dev/null +++ b/Sources/unused-imports/Reporters/UnusedImportReporter.swift @@ -0,0 +1,3 @@ +protocol UnusedImportReporter { + func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) +} diff --git a/Sources/unused-imports/SourceFileWithUnusedImports.swift b/Sources/unused-imports/SourceFileWithUnusedImports.swift new file mode 100644 index 0000000..a755ee1 --- /dev/null +++ b/Sources/unused-imports/SourceFileWithUnusedImports.swift @@ -0,0 +1,8 @@ +struct SourceFileWithUnusedImports: Codable, Comparable { + let path: String + let unusedImportStatements: [UnusedImportStatement] + + static func <(lhs: SourceFileWithUnusedImports, rhs: SourceFileWithUnusedImports) -> Bool { + return lhs.path < rhs.path + } +} diff --git a/Sources/unused-imports/UnusedImportStatement.swift b/Sources/unused-imports/UnusedImportStatement.swift new file mode 100644 index 0000000..0dfceac --- /dev/null +++ b/Sources/unused-imports/UnusedImportStatement.swift @@ -0,0 +1,8 @@ +struct UnusedImportStatement: Codable, Comparable { + let moduleName: String + let lineNumber: Int + + static func <(lhs: UnusedImportStatement, rhs: UnusedImportStatement) -> Bool { + return lhs.moduleName < rhs.moduleName + } +} diff --git a/Sources/unused-imports/main.swift b/Sources/unused-imports/main.swift index e302ee3..e20f606 100644 --- a/Sources/unused-imports/main.swift +++ b/Sources/unused-imports/main.swift @@ -6,6 +6,7 @@ private typealias References = (usrs: Set, typealiases: Set) private let identifierRegex = try Regex("([a-zA-Z_][a-zA-Z0-9_]*)") private let ignoreRegex = try Regex(#"// *@ignore-import$"#) private var cachedLines = [String: [String.SubSequence]]() +private let defaultReporter = SedCommandReporter() private struct Configuration: Decodable { static func attemptingPath(_ path: String?) -> Configuration? { @@ -23,17 +24,20 @@ private struct Configuration: Decodable { let ignoredFileRegex: Regex? let ignoredModuleRegex: Regex? let alwaysKeepImports: Set + let reporter: UnusedImportReporter private enum CodingKeys: String, CodingKey { case ignoredFileRegex = "ignored-file-regex" case ignoredModuleRegex = "ignored-module-regex" case alwaysKeepImports = "always-keep-imports" + case reporter = "reporter" } init() { self.alwaysKeepImports = [] self.ignoredFileRegex = nil self.ignoredModuleRegex = nil + self.reporter = defaultReporter } init(from decoder: Decoder) throws { @@ -51,6 +55,23 @@ private struct Configuration: Decodable { } else { self.ignoredModuleRegex = nil } + + if let string = try values.decodeIfPresent(String.self, forKey: .reporter) { + if string == "json" { + self.reporter = JSONReporter() + } else { + let invalidReporterTypeErrorMessage = """ +error: requested a type of reporter that doesn't exist: `\(string)`." +In your unused-imports configuration try either: + + 1. Removing the `reporter` key to get the default `sed` command reporter or + 2. Setting the `reporter` key to `json` to get the JSON reporter +""" + fatalError(invalidReporterTypeErrorMessage) + } + } else { + self.reporter = defaultReporter + } } func shouldIgnoreFile(_ file: String) -> Bool { @@ -68,6 +89,10 @@ private struct Configuration: Decodable { return false } + + func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) { + self.reporter.didFind(sourceFilesWithUnusedImports: sourceFilesWithUnusedImports) + } } private func getImports(path: String, recordReader: RecordReader) -> (Set, [String: Int]) { @@ -191,6 +216,8 @@ private func main( usrs: definedUsrs, typealiases: definedTypealiases) } + var sourceFilesWithUnusedImports: [SourceFileWithUnusedImports] = [] + for (unitReader, recordReader) in unitsAndRecords { if configuration.shouldIgnoreFile(unitReader.mainFile) { continue @@ -235,10 +262,16 @@ private func main( let unusedImports = allImports.subtracting(usedImports).subtracting(configuration.alwaysKeepImports) if !unusedImports.isEmpty { - let sedCmd = unusedImports.map { importsToLineNumbers[$0]! }.sorted().map { "\($0)d" }.joined(separator: ";") - let relativePath = unitReader.mainFile.replacingOccurrences(of: pwd + "/", with: "") - print("/usr/bin/sed -i \"\" '\(sedCmd)' '\(relativePath)'") - } + let sourceFileWithUnusedImports = SourceFileWithUnusedImports( + path: unitReader.mainFile.replacingOccurrences(of: pwd + "/", with: ""), + unusedImportStatements: unusedImports.map { UnusedImportStatement(moduleName: $0, lineNumber: importsToLineNumbers[$0]!) }.sorted() + ) + sourceFilesWithUnusedImports.append(sourceFileWithUnusedImports) + } + } + + if sourceFilesWithUnusedImports.count != 0 { + configuration.didFind(sourceFilesWithUnusedImports: sourceFilesWithUnusedImports) } }