Skip to content

Commit a606a4e

Browse files
authored
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
1 parent 9c89147 commit a606a4e

7 files changed

+82
-4
lines changed

Sources/unused-imports/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ swift_binary(
44
name = "unused-imports",
55
srcs = [
66
"main.swift",
7+
"SourceFileWithUnusedImports.swift",
8+
"UnusedImportStatement.swift",
9+
"Reporters/JSONReporter.swift",
10+
"Reporters/SedCommandReporter.swift",
11+
"Reporters/UnusedImportReporter.swift",
712
],
813
tags = [
914
"manual",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
struct JSONReporter: UnusedImportReporter {
4+
func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) {
5+
let jsonEncoder = JSONEncoder()
6+
let removableImportsJSONData = try! jsonEncoder.encode(sourceFilesWithUnusedImports)
7+
let removableImportsJSONString = String(data: removableImportsJSONData, encoding: String.Encoding.utf8)!
8+
9+
print(removableImportsJSONString)
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
struct SedCommandReporter: UnusedImportReporter {
4+
func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) {
5+
for sourceFile in sourceFilesWithUnusedImports.sorted() {
6+
let sedCmd = sourceFile.unusedImportStatements.map { unusedImport in "\(unusedImport.lineNumber)d" }.joined(separator: ";")
7+
print("/usr/bin/sed -i \"\" '\(sedCmd)' '\(sourceFile.path)'")
8+
}
9+
}
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
protocol UnusedImportReporter {
2+
func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports])
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
struct SourceFileWithUnusedImports: Codable, Comparable {
2+
let path: String
3+
let unusedImportStatements: [UnusedImportStatement]
4+
5+
static func <(lhs: SourceFileWithUnusedImports, rhs: SourceFileWithUnusedImports) -> Bool {
6+
return lhs.path < rhs.path
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
struct UnusedImportStatement: Codable, Comparable {
2+
let moduleName: String
3+
let lineNumber: Int
4+
5+
static func <(lhs: UnusedImportStatement, rhs: UnusedImportStatement) -> Bool {
6+
return lhs.moduleName < rhs.moduleName
7+
}
8+
}

Sources/unused-imports/main.swift

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ private typealias References = (usrs: Set<String>, typealiases: Set<String>)
66
private let identifierRegex = try Regex("([a-zA-Z_][a-zA-Z0-9_]*)")
77
private let ignoreRegex = try Regex(#"// *@ignore-import$"#)
88
private var cachedLines = [String: [String.SubSequence]]()
9+
private let defaultReporter = SedCommandReporter()
910

1011
private struct Configuration: Decodable {
1112
static func attemptingPath(_ path: String?) -> Configuration? {
@@ -23,17 +24,20 @@ private struct Configuration: Decodable {
2324
let ignoredFileRegex: Regex<AnyRegexOutput>?
2425
let ignoredModuleRegex: Regex<AnyRegexOutput>?
2526
let alwaysKeepImports: Set<String>
27+
let reporter: UnusedImportReporter
2628

2729
private enum CodingKeys: String, CodingKey {
2830
case ignoredFileRegex = "ignored-file-regex"
2931
case ignoredModuleRegex = "ignored-module-regex"
3032
case alwaysKeepImports = "always-keep-imports"
33+
case reporter = "reporter"
3134
}
3235

3336
init() {
3437
self.alwaysKeepImports = []
3538
self.ignoredFileRegex = nil
3639
self.ignoredModuleRegex = nil
40+
self.reporter = defaultReporter
3741
}
3842

3943
init(from decoder: Decoder) throws {
@@ -51,6 +55,23 @@ private struct Configuration: Decodable {
5155
} else {
5256
self.ignoredModuleRegex = nil
5357
}
58+
59+
if let string = try values.decodeIfPresent(String.self, forKey: .reporter) {
60+
if string == "json" {
61+
self.reporter = JSONReporter()
62+
} else {
63+
let invalidReporterTypeErrorMessage = """
64+
error: requested a type of reporter that doesn't exist: `\(string)`."
65+
In your unused-imports configuration try either:
66+
67+
1. Removing the `reporter` key to get the default `sed` command reporter or
68+
2. Setting the `reporter` key to `json` to get the JSON reporter
69+
"""
70+
fatalError(invalidReporterTypeErrorMessage)
71+
}
72+
} else {
73+
self.reporter = defaultReporter
74+
}
5475
}
5576

5677
func shouldIgnoreFile(_ file: String) -> Bool {
@@ -68,6 +89,10 @@ private struct Configuration: Decodable {
6889

6990
return false
7091
}
92+
93+
func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports]) {
94+
self.reporter.didFind(sourceFilesWithUnusedImports: sourceFilesWithUnusedImports)
95+
}
7196
}
7297

7398
private func getImports(path: String, recordReader: RecordReader) -> (Set<String>, [String: Int]) {
@@ -191,6 +216,8 @@ private func main(
191216
usrs: definedUsrs, typealiases: definedTypealiases)
192217
}
193218

219+
var sourceFilesWithUnusedImports: [SourceFileWithUnusedImports] = []
220+
194221
for (unitReader, recordReader) in unitsAndRecords {
195222
if configuration.shouldIgnoreFile(unitReader.mainFile) {
196223
continue
@@ -235,10 +262,16 @@ private func main(
235262

236263
let unusedImports = allImports.subtracting(usedImports).subtracting(configuration.alwaysKeepImports)
237264
if !unusedImports.isEmpty {
238-
let sedCmd = unusedImports.map { importsToLineNumbers[$0]! }.sorted().map { "\($0)d" }.joined(separator: ";")
239-
let relativePath = unitReader.mainFile.replacingOccurrences(of: pwd + "/", with: "")
240-
print("/usr/bin/sed -i \"\" '\(sedCmd)' '\(relativePath)'")
241-
}
265+
let sourceFileWithUnusedImports = SourceFileWithUnusedImports(
266+
path: unitReader.mainFile.replacingOccurrences(of: pwd + "/", with: ""),
267+
unusedImportStatements: unusedImports.map { UnusedImportStatement(moduleName: $0, lineNumber: importsToLineNumbers[$0]!) }.sorted()
268+
)
269+
sourceFilesWithUnusedImports.append(sourceFileWithUnusedImports)
270+
}
271+
}
272+
273+
if sourceFilesWithUnusedImports.count != 0 {
274+
configuration.didFind(sourceFilesWithUnusedImports: sourceFilesWithUnusedImports)
242275
}
243276
}
244277

0 commit comments

Comments
 (0)