Sources/SwiftCodeSanKit/Operations/RemoveUnusedImports.swift (109 lines of code) (raw):
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
public func removeUnusedImports(fileToModuleMap: [String: String],
whitelist: Whitelist?,
topDeclsOnly: Bool,
inplace: Bool,
logFilePath: String? = nil,
concurrencyLimit: Int? = nil) {
scanConcurrencyLimit = concurrencyLimit
let p = DeclParser()
log("Scan all decls and generate a decl map...")
logTime()
let allDeclMap = p.scanAndMapDecls(fileToModuleMap: fileToModuleMap,
topDeclsOnly: topDeclsOnly)
logTime()
log("#Decls", allDeclMap.keys.count)
var unusedImports = [String: [String]]()
let whitelistModulesBlock = { (module: String) -> Bool in
let moduleComps = module.components(separatedBy: ".").filter {!$0.isEmpty}
for comp in moduleComps {
if let list = whitelist?.modules, list.contains(comp) {
return true
}
if let list = whitelist?.modulesSuffix {
for suffix in list {
if comp.hasSuffix(suffix) {
return true
}
}
}
if let list = whitelist?.modulesPrefix {
for prefix in list {
if comp.hasPrefix(prefix) {
return true
}
}
}
}
return false
}
log("Check referenced decls and compare their source modules against imported modules to filter out unused imports...")
var total = 0
p.checkRefs(fileToModuleMap: fileToModuleMap, declMap: allDeclMap) { (filepath, refs, imports) in
var usedImportsInFile = [String: Bool]()
for i in imports {
usedImportsInFile[i] = whitelistModulesBlock(i)
}
for r in refs {
if let refDecls = allDeclMap[r] {
for refDecl in refDecls {
let m = refDecl.module
if imports.contains(m) {
usedImportsInFile[m] = true
} else {
let refinedImports = imports.filter {$0.contains(".")}
for item in refinedImports {
let comps = item.components(separatedBy: ".")
if comps.contains(m) {
usedImportsInFile[item] = true
}
}
}
}
} else if imports.contains(r) {
// Sometimes a module name can be used in code, e.g. CoreFoundation.Foo
usedImportsInFile[r] = true
}
}
var unusedListInFile = [String]()
for (module, used) in usedImportsInFile {
if !used {
total += 1
unusedListInFile.append(module)
}
}
if !unusedListInFile.isEmpty {
unusedImports[filepath] = Set(unusedListInFile).compactMap{$0}
}
// log(total, interval: 200)
}
logTime()
log("#Unused imports", total)
if let op = logFilePath {
log("Save results...")
var totalUnused = 0
var ret = unusedImports.map { (path, unusedlist) -> String in
totalUnused += unusedlist.count
return path + "\n" + String(unusedlist.count) + "\n" + unusedlist.joined(separator: ", ")
}
assert(total == totalUnused)
ret.append("Total unused: \(totalUnused)")
let retStr = ret.joined(separator: "\n\n")
let declstr = allDeclMap.map{ (k, v) -> String in
let t = """
\(k): \(v.map { $0.path }.joined(separator: ", "))
"""
return t
}.joined(separator: "\n")
try? retStr.write(toFile: op, atomically: true, encoding: .utf8)
try? declstr.write(toFile: op+"-decls", atomically: true, encoding: .utf8)
}
if inplace {
log("Remove unused imports from files...", unusedImports.keys.count)
let updater = DeclUpdater()
updater.removeUnusedImports(fileToModuleMap: fileToModuleMap,
unusedImports: unusedImports) { (path, result) in
try? result.write(toFile: path, atomically: true, encoding: .utf8)
}
}
logTime()
logTotalElapsed("Done")
}