Sources/SwiftCodeSanKit/Operations/UpdateAccessLevels.swift (323 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 updateAccessLevels(filesToModules: [String: String], whitelist: Whitelist?, inplace: Bool, logFilePath: String? = nil, concurrencyLimit: Int? = nil, onCompletion: @escaping () -> ()) { 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() log("Check references, look up their source modules, and mark visibility...") p.checkRefs(fileToModuleMap: filesToModules, declMap: declMap) { (path, refs, imports) in if let refModule = filesToModules[path] { markVisiblity(refs, in: refModule, imports: imports, with: declMap, updateMembers: true) } } logTime() log("Update ALs (access levels) of top-level decls and their bound types...") updateBoundTypeALs(declMap: declMap) resetVisited(declMap: declMap) log("Update ALs of member decls of interfaces (protocol / base class)...") updateMemberALs(declMap: declMap) logTime() log("Flatten decls, and check references again, for member decls...") var nref = 0 let flatDeclMap = flatten(declMap: declMap) p.checkRefs(fileToModuleMap: filesToModules, declMap: flatDeclMap) { (path, refs, imports) in if let refModule = filesToModules[path] { markVisiblity(refs, in: refModule, imports: imports, with: flatDeclMap, updateMembers: false) } log(counter: &nref, interval: 1000) } log(nref) logTime() log("Update ALs of all decls and their bound types...") updateBoundTypeALs(declMap: flatDeclMap) resetVisited(declMap: flatDeclMap) var i = -1 var j = 0 while i != j { log("If bound types are modified, update their member ALs as well...") updateMemberALs(declMap: declMap) i = flatDeclMap.values.flatMap{$0}.filter{$0.shouldExpose}.count log("Again, update ALs of of all decls and their bound types...") updateBoundTypeALs(declMap: flatDeclMap) resetVisited(declMap: flatDeclMap) j = flatDeclMap.values.flatMap{$0}.filter{$0.shouldExpose}.count log("#Remaining decls to update", i-j, i, j) } log("Save decls to update per files...") for (_, decls) in flatDeclMap { for decl in decls { if decl.isPublicOrOpen, !decl.shouldExpose { if pathToDeclsUpdate[decl.path] == nil { pathToDeclsUpdate[decl.path] = [] } pathToDeclsUpdate[decl.path]?.append(decl) } } } if let logfile = logFilePath { log("Save results to", logfile) let ret = pathToDeclsUpdate.map {"\($0.key): \($0.value.map{$0.name + ", " + $0.encloser}.joined(separator: "\n"))"}.joined(separator: "\n") try? ret.write(toFile: logfile, atomically: true, encoding: .utf8) } if inplace { log("Update decl ALs in files...") let updater = DeclUpdater() updater.updateAccessLevels(filesToDecls: pathToDeclsUpdate, filesToModules: filesToModules) { (path, content) in try? content.write(toFile: path, atomically: true, encoding: .utf8) } } logTime() let total = pathToDeclsUpdate.values.flatMap{$0}.count log("#Total top-level decls: ", declMap.count, "#Total decls", flatDeclMap.count, "#Decls updated", total, "#Files updated", pathToDeclsUpdate.count) logTotalElapsed("Done") onCompletion() } // MARK - private functions private func updateBoundTypeALs(declMap: DeclMap) { for (k, decls) in declMap { if !k.isEmpty { // Empty means expr or stmt for decl in decls { if (decl.isPublicOrOpen && decl.shouldExpose) || decl.declType == .extensionType || decl.isExtensionMember { decl.visited = true updateBoundTypeALs(decl, level: 0, declMap: declMap) } } } } } private func updateBoundTypeALs(_ decl: DeclMetadata, level: Int, declMap: DeclMap) { for boundType in decl.boundTypesAL { 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.shouldExpose { continue } boundDecl.visited = true if decl.module == boundDecl.module || decl.imports.contains(boundDecl.module) { boundDecl.shouldExpose = true updateBoundTypeALs(boundDecl, level: level + 1, declMap: declMap) } } } } } } private func updateMemberALs(declMap: DeclMap) { for (_, vals) in declMap { for cur in vals { var members = [DeclMetadata]() var interfaceMembers = [DeclMetadata]() let level = 0 updateBoundMemberALs(key: cur, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers) } } } private func updateBoundMemberALs(key cur: DeclMetadata, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata]) { // 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) let interfaceMemberNames = interfaceMembers.map{$0.name} for member in members { if interfaceMemberNames.contains(member.name) { if member.isPublicOrOpen || (curIsExtension && cur.isPublicOrOpen) { member.shouldExpose = true // If encloser is extension, it should be also exposed since its member is public/exposed if curIsExtension, !cur.shouldExpose { cur.shouldExpose = true } } } else if member.isPublicOrOpen, member.isOverride { // This might be a member overriding stdlib api member.shouldExpose = true } } // For the following decl types, check bound types and update member ALs. if cur.declType == .extensionType || cur.declType == .enumType { var visitedCurrent = false let boundTypesAL = cur.boundTypesAL.filter{!cur.inheritedTypes.contains($0)} for boundType in boundTypesAL { if !boundType.isEmpty, cur.name != boundType, let boundTypeVals = declMap[boundType] { for boundDecl in boundTypeVals { if !visitedCurrent, boundDecl.isPublicOrOpen, boundDecl.shouldExpose { for member in cur.members { if member.isPublicOrOpen { member.shouldExpose = true } } visitedCurrent = true } } } else if !visitedCurrent, cur.inheritedTypes.contains(boundType) { // If parent is not in declMap, assume it's in stdlib. for member in cur.members { if member.isPublicOrOpen { member.shouldExpose = true } } visitedCurrent = true } } if visitedCurrent, !cur.shouldExpose { cur.shouldExpose = true } } } private func resolveInheritance(key cur: DeclMetadata, inheritedTypes: [String]?, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata]) { let parents = inheritedTypes ?? cur.inheritedTypes var stdlibTypes = [String]() var userDefinedTypes = [String]() 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.isPublicOrOpen, parentDecl.shouldExpose { 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.boundTypesAL : nil resolveInheritance(key: parentDecl, inheritedTypes: optionalInitialTypes, declMap: declMap, level: level+1, members: &members, interfaceMembers: &interfaceMembers) } 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) } } } else { // If parent is not in declMap, assume it's in stdlib. stdlibTypes.append(parent) } } for stdlibType in stdlibTypes { if userDefinedTypes.contains(stdlibType) { continue } for member in cur.members { if member.isPublicOrOpen { interfaceMembers.append(member) members.append(member) } } break } } private func traverseMembers(_ 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 checked = traverseMembers(bases, i + 1, refModule, imports, declMap: declMap) if checked, refModule != prefixDecl.module, imports.contains(prefixDecl.module) { for member in list { member.shouldExpose = true } } } else { return false } } } } return true } private func markVisiblity(_ 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 = traverseMembers(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 true || refDecl.isPublicOrOpen || refDecl.declType == .extensionType || refDecl.isExtensionMember { // multi modules w/ same decls (foo): // 1. shadowing: if ref'd, it uses a decl in the same module even if the others are imported. // - if foo from another module should be called, it's required to use qualifier X.foo // 2. if not decl's in the same module as ref, uses corresponding modules, so need to look up imports // 3. if foo inits are the same for multi-modules: // - need qualifier X.foo if refModule == refDecl.module { // r is either declared internally // so r should not be public, so add [decl.path: r] to pathToUpdateDecls } else { // look up imports and check decl.module is in the imports, then decl.shouldBePublic = true, so do nothing. if imports.contains(refDecl.module) { if !refDecl.encloser.isEmpty { // If it has an encloser (part of a class, protocol, etc), // check if the encloser is in ref'd. // Encloser type might not be listed, leakdetect.inst.accumulatedLeaksStream refDecl.shouldExpose = true } else { // then r in decl.module should remain public refDecl.shouldExpose = true } } else { // r must be part of stdlib, handled in updateMemberALs above. } } } } } } } private func shouldMatchACLForMembers(_ declType: DeclType) -> Bool { return declType == .protocolType || declType == .extensionType || declType == .enumType } private func resetVisited(declMap: DeclMap) { for (_, decls) in declMap { for decl in decls { decl.visited = false } } }