Skip to content

Commit

Permalink
Add optional JSON output (#52)
Browse files Browse the repository at this point in the history
- Allow users to select a custom reporter that prints JSON instead of
`sed` commands

- Opt-in by adding `"reporter": "json"` to the configuration file
  • Loading branch information
wileykestner authored Aug 3, 2023
1 parent 9c89147 commit a606a4e
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 4 deletions.
5 changes: 5 additions & 0 deletions Sources/unused-imports/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions Sources/unused-imports/Reporters/JSONReporter.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions Sources/unused-imports/Reporters/SedCommandReporter.swift
Original file line number Diff line number Diff line change
@@ -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)'")
}
}
}
3 changes: 3 additions & 0 deletions Sources/unused-imports/Reporters/UnusedImportReporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
protocol UnusedImportReporter {
func didFind(sourceFilesWithUnusedImports: [SourceFileWithUnusedImports])
}
8 changes: 8 additions & 0 deletions Sources/unused-imports/SourceFileWithUnusedImports.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions Sources/unused-imports/UnusedImportStatement.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
41 changes: 37 additions & 4 deletions Sources/unused-imports/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ private typealias References = (usrs: Set<String>, typealiases: Set<String>)
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? {
Expand All @@ -23,17 +24,20 @@ private struct Configuration: Decodable {
let ignoredFileRegex: Regex<AnyRegexOutput>?
let ignoredModuleRegex: Regex<AnyRegexOutput>?
let alwaysKeepImports: Set<String>
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 {
Expand All @@ -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 {
Expand All @@ -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>, [String: Int]) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down

0 comments on commit a606a4e

Please sign in to comment.