Sources/SwiftCodeSanKit/Operations/RemoveDeadDecls.swift (324 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 removeDeadDecls(filesToModules: [String: String], whitelist: Whitelist?, topDeclsOnly: Bool, inplace: Bool, testFiles: [String]?, inplaceTests: Bool, logFilePath: String? = nil, concurrencyLimit: Int? = nil, onCompletion: @escaping () -> ()) { log("Start of removing dead code: topDeclsOnly", topDeclsOnly) scanConcurrencyLimit = concurrencyLimit let p = DeclParser() var pathToDeclsUpdate = [String: [DeclMetadata]]() log("Scan and map top-level decls...") logTime() let declMap = p.scanAndMapDecls(fileToModuleMap: filesToModules, topDeclsOnly: false, whitelist: whitelist) logTime() print("WWW: ", p.npaths, p.wpaths) log("Check references, look up their source modules, and mark used...") let flatDeclMap = flatten(declMap: declMap) var nref = 0 p.checkRefs(fileToModuleMap: filesToModules, declMap: flatDeclMap) { (path, refs, imports) in if let refModule = filesToModules[path] { markUsed(refs, in: refModule, imports: imports, with: flatDeclMap, updateMembers: true) } log("#Checked refs", counter: &nref, interval: 1000, timed: true) } logTime() repeat { log("Look up interface members and mark used if any...") shouldRetry = false markInterfaceMembersUsed(declMap: declMap) logTime() } while shouldRetry var i = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count log("Mark bound types used...") markBoundTypesUsed(declMap: flatDeclMap) resetVisited(declMap: declMap) var j = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() while i != j { log("#Remaining decls to mark used", j-i, j, i) log("Repeat: Look up interface members and mark used if any...") markInterfaceMembersUsed(declMap: declMap) i = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() log("Repeat: Mark bound types used...") markBoundTypesUsed(declMap: flatDeclMap) resetVisited(declMap: flatDeclMap) j = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() } log("Filter out used decls...") for (_, decls) in flatDeclMap { for decl in decls { if !decl.used { if pathToDeclsUpdate[decl.path] == nil { pathToDeclsUpdate[decl.path] = [] } pathToDeclsUpdate[decl.path]?.append(decl) } } } let totalUnused = pathToDeclsUpdate.values.flatMap{$0}.count let totalUsed = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() log("#Total top-level decls: ", declMap.values.flatMap{$0}.count, "#Total decls", flatDeclMap.values.flatMap{$0}.count, "#Decls Unused", totalUnused, "#Decls Used", totalUsed, "#Files to update", pathToDeclsUpdate.count) if let logfile = logFilePath { log("Save results to", logfile) let ret = pathToDeclsUpdate.map { arg in let vals = arg.value.map{ ObjectIdentifier($0).debugDescription + ", " + $0.fullName + ", " + $0.encloser } let valStr = vals.joined(separator: "\n") return "\(arg.key)\n--- \(valStr)\n" }.joined(separator: "\n") try? ret.write(toFile: logfile, atomically: true, encoding: .utf8) logTime() } if inplace { log("Remove unused decls from files...", pathToDeclsUpdate.count) let updater = DeclUpdater() updater.removeDeadDecls(filesToDecls: pathToDeclsUpdate) { (path, content) in try? content.write(toFile: path, atomically: true, encoding: .utf8) } logTime() } logTotalElapsed("Done") onCompletion() } // MARK - private functions private func markBoundTypesUsed(declMap: DeclMap) { for (k, decls) in declMap { if !k.isEmpty { // Empty means expr or stmt for decl in decls { if decl.used { decl.visited = true markBoundTypesUsed(decl, level: 0, declMap: declMap) } } } } } private func markBoundTypesUsed(_ decl: DeclMetadata, level: Int, declMap: DeclMap) { for boundType in decl.boundTypes { if !boundType.isEmpty { var bases: [String]? var leaf: String? if boundType.contains(".") { bases = boundType.components(separatedBy: ".") leaf = bases?.removeLast() } let key = leaf ?? boundType if let boundDecls = declMap[key] { for boundDecl in boundDecls { if boundDecl.visited, boundDecl.used { continue } boundDecl.visited = true if decl.module == boundDecl.module || decl.imports.contains(boundDecl.module) { boundDecl.used = true markBoundTypesUsed(boundDecl, level: level + 1, declMap: declMap) } } } } } } var shouldRetry = false private func markInterfaceMembersUsed(declMap: DeclMap) { var ndecls = 0 scan(declMap) { (key, vals, lock) in for cur in vals { var members = [DeclMetadata]() var interfaceMembers = [DeclMetadata]() var userDefinedTypes = [String]() var stdlibTypes = [String]() let level = 0 markBoundMembersUsed(key: cur, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers, userDefinedTypes: &userDefinedTypes, stdlibTypes: &stdlibTypes) log("#Marked used members", counter: &ndecls, interval: 10000, timed: true) } } } private func markBoundMembersUsed(key cur: DeclMetadata, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata], userDefinedTypes: inout [String], stdlibTypes: inout [String]) { // First resolve inheritance (loop up protocol conformance, subclassing, and update member ALs) var parents = cur.inheritedTypes let curIsExtension = cur.declType == .extensionType if curIsExtension { parents.append(cur.name) } resolveInheritance(key: cur, inheritedTypes: parents, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers, userDefinedTypes: &userDefinedTypes, stdlibTypes: &stdlibTypes) let stdTypes = stdlibTypes.filter{!userDefinedTypes.contains($0)} for member in members { let matchingMembers = interfaceMembers.filter {$0.name == member.name} for matched in matchingMembers { if matched.used { member.used = true } else if member.used || member.isOverride { if !matched.used { matched.used = true shouldRetry = true } } else if !stdTypes.isEmpty { if !matched.used { matched.used = true shouldRetry = true } member.used = true } } if matchingMembers.isEmpty, member.isOverride { // This might be a member overriding stdlib api member.used = true } } // For the following decl types, check bound types and update member ALs. if cur.declType == .extensionType || cur.declType == .enumType { if !cur.used { for m in cur.members { if m.used { cur.used = true break } } } if !cur.used { let boundTypes = cur.boundTypes.filter{!cur.inheritedTypes.contains($0)} for boundType in boundTypes { if boundType.isEmpty { continue } if cur.name != boundType, let _ = declMap[boundType] { // even if boundtype is used, cur might not be used } else if cur.inheritedTypes.contains(boundType) { // If parent is not in declMap, assume it's in stdlib. for member in cur.members { member.used = true cur.used = true } if cur.used { break } } } } } } private func resolveInheritance(key cur: DeclMetadata, inheritedTypes: [String]?, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata], userDefinedTypes: inout [String], stdlibTypes: inout [String]) { let parents = inheritedTypes ?? cur.inheritedTypes for parent in parents { if parent.isEmpty { continue } if let parentDecls = declMap[parent] { for parentDecl in parentDecls { if parentDecl.name.isEmpty { continue } if parentDecl.declType == .protocolType || parentDecl.declType == .classType || parentDecl.declType == .typealiasType { if parentDecl.declType == .protocolType { interfaceMembers.append(contentsOf: parentDecl.members) } else if parentDecl.declType == .classType, cur.declType == .classType { interfaceMembers.append(contentsOf: parentDecl.members) } userDefinedTypes.append(parentDecl.name) members.append(contentsOf: cur.members) let optionalInitialTypes = parentDecl.declType == .typealiasType ? parentDecl.boundTypes : nil resolveInheritance(key: parentDecl, inheritedTypes: optionalInitialTypes, declMap: declMap, level: level+1, members: &members, interfaceMembers: &interfaceMembers, userDefinedTypes: &userDefinedTypes, stdlibTypes: &stdlibTypes) } else if parentDecl.declType == .extensionType { // Parent could be a user defined type or a stdlib type. Add to a list for now and filter out below. stdlibTypes.append(parentDecl.name) members.append(contentsOf: cur.members) } } } else { // If parent is not in declMap, assume it's in stdlib. stdlibTypes.append(parent) } } for stdlibType in stdlibTypes { if userDefinedTypes.contains(stdlibType) { continue } interfaceMembers.append(contentsOf: cur.members) members.append(contentsOf: cur.members) break } } private func accessMembers(_ bases: [String], _ i: Int, _ refModule: String, _ imports: [String], declMap: DeclMap) -> Bool { let j = i + 1 if j < bases.count { let cur = bases[i] let next = bases[j] if let prefixDecls = declMap[cur] { for prefixDecl in prefixDecls { var list: [DeclMetadata]? if prefixDecl.declType == .funcType || prefixDecl.declType == .operatorType || // This is handled here but shouldn't be member-accessed prefixDecl.declType == .varType { if let typeDecls = declMap[prefixDecl.type] { for t in typeDecls { list = t.members.filter{$0.name == next} } } } else { list = prefixDecl.members.filter{$0.name == next} } if let list = list, !list.isEmpty { let accessed = accessMembers(bases, i + 1, refModule, imports, declMap: declMap) if accessed, (refModule == prefixDecl.module || imports.contains(prefixDecl.module)) { for member in list { member.used = true } } } else { return false } } } } return true } private func markUsed(_ refs: Set<String>, in refModule: String, imports: [String], with declMap: DeclMap, updateMembers: Bool) { // Leaf level node checks for r in refs { var bases: [String]? var leaf: String? if r.contains(".") { bases = r.components(separatedBy: ".") } // First, traverse member access, and update visibility along the way var accessedMembers = false if let bases = bases { accessedMembers = accessMembers(bases, 0, refModule, imports, declMap: declMap) } if accessedMembers { continue } leaf = bases?.removeLast() let refKey = leaf ?? r // If above fails (e.g. encloser type is not found), or non-member access, try following if let refDecls = declMap[refKey] { for refDecl in refDecls { if refModule == refDecl.module || imports.contains(refDecl.module) || refDecl.isOverride { refDecl.used = true } } } } } private func resetVisited(declMap: DeclMap) { for (_, decls) in declMap { for decl in decls { decl.visited = false } } }