├── .gitignore ├── Sources ├── RestrictCallCore │ ├── Extension │ │ ├── String+.swift │ │ └── IndexStoreSymbol+.swift │ ├── Model │ │ └── RestrictedTarget.swift │ ├── Util │ │ └── demangle.swift │ └── RestrictCallReporter.swift └── restrict-call │ ├── Extension │ └── ReportType+.swift │ ├── Model │ └── Config.swift │ └── restrict_call.swift ├── .swift-restrict-call.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md └── Plugins └── RestrictCallBuildToolPlugin └── plugin.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Sources/RestrictCallCore/Extension/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+.swift 3 | // swift-restrict-call 4 | // 5 | // Created by p-x9 on 2025/08/15 6 | // 7 | // 8 | 9 | extension String { 10 | package func matches(pattern: String) -> Bool { 11 | self.range(of: pattern, options: .regularExpression) != nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/restrict-call/Extension/ReportType+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReportType+.swift 3 | // swift-restrict-call 4 | // 5 | // Created by p-x9 on 2025/08/15 6 | // 7 | // 8 | 9 | import Foundation 10 | import ArgumentParser 11 | import SourceReporter 12 | import RestrictCallCore 13 | 14 | extension ReportType: @retroactive ExpressibleByArgument {} 15 | -------------------------------------------------------------------------------- /Sources/restrict-call/Model/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // swift-restrict-call 4 | // 5 | // Created by p-x9 on 2025/08/15 6 | // 7 | // 8 | 9 | import Foundation 10 | import SourceReporter 11 | import RestrictCallCore 12 | 13 | struct Config: Codable { 14 | var defaultReportType: ReportType? 15 | var targets: [RestrictedTarget] 16 | var excludedFiles: [String]? 17 | 18 | var onlyModules: [String]? 19 | var excludeModules: [String]? 20 | } 21 | -------------------------------------------------------------------------------- /.swift-restrict-call.yml: -------------------------------------------------------------------------------- 1 | defaultReportType: warning 2 | targets: 3 | - reportType: error 4 | module: "Foundation" 5 | type: "URL" 6 | name: "init\\(string:\\)" 7 | - reportType: error 8 | module: "Foundation" 9 | type: "URL" 10 | name: "path$" 11 | - reportType: error 12 | module: "Foundation" 13 | type: "String" 14 | name: "init\\(data" 15 | # onlyModules: 16 | # - RestrictCallCore 17 | # excludeModules: 18 | # - restrict-call 19 | excludedFiles: 20 | - ".*/DerivedData/.*" 21 | -------------------------------------------------------------------------------- /Sources/RestrictCallCore/Model/RestrictedTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestrictedTarget.swift 3 | // swift-restrict-call 4 | // 5 | // Created by p-x9 on 2025/08/15 6 | // 7 | // 8 | 9 | import Foundation 10 | import SourceReporter 11 | 12 | public struct RestrictedTarget: Sendable, Codable { 13 | public let reportType: ReportType? 14 | public let module: String? 15 | public let type: String? 16 | public let name: String 17 | 18 | public init( 19 | reportType: ReportType?, 20 | module: String?, 21 | type: String?, 22 | name: String 23 | ) { 24 | self.reportType = reportType 25 | self.module = module 26 | self.type = type 27 | self.name = name 28 | } 29 | } 30 | 31 | extension RestrictedTarget { 32 | var demangledNamePattern: String { 33 | [module, type, name] 34 | .compactMap { $0 } 35 | .joined(separator: "\\.") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 p-x9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "5df3a919a9f5dd17f896e369713a343c155fb4622aad89f536ea8f9607c53124", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser.git", 8 | "state" : { 9 | "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", 10 | "version" : "1.6.1" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-indexstore", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/kateinoigakukun/swift-indexstore.git", 17 | "state" : { 18 | "revision" : "055264ececff53c1265c2b7ee3513053b02604de", 19 | "version" : "0.4.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-source-reporter", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/p-x9/swift-source-reporter.git", 26 | "state" : { 27 | "revision" : "702db03a2b7277e89f5859dd7b260160636d1187", 28 | "version" : "0.2.0" 29 | } 30 | }, 31 | { 32 | "identity" : "yams", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/jpsim/Yams.git", 35 | "state" : { 36 | "revision" : "3d6871d5b4a5cd519adf233fbb576e0a2af71c17", 37 | "version" : "5.4.0" 38 | } 39 | } 40 | ], 41 | "version" : 3 42 | } 43 | -------------------------------------------------------------------------------- /Sources/RestrictCallCore/Util/demangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // demangle.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/14 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | @_silgen_name("swift_demangle") 12 | internal func _stdlib_demangleImpl( 13 | mangledName: UnsafePointer?, 14 | mangledNameLength: UInt, 15 | outputBuffer: UnsafeMutablePointer?, 16 | outputBufferSize: UnsafeMutablePointer?, 17 | flags: UInt32 18 | ) -> UnsafeMutablePointer? 19 | 20 | internal func stdlib_demangleName( 21 | _ mangledName: String 22 | ) -> String { 23 | guard !mangledName.isEmpty else { return mangledName } 24 | return mangledName.utf8CString.withUnsafeBufferPointer { mangledNameUTF8 in 25 | let demangledNamePtr = _stdlib_demangleImpl( 26 | mangledName: mangledNameUTF8.baseAddress, 27 | mangledNameLength: numericCast(mangledNameUTF8.count - 1), 28 | outputBuffer: nil, 29 | outputBufferSize: nil, 30 | flags: 0 31 | ) 32 | 33 | if let demangledNamePtr { 34 | return String(cString: demangledNamePtr) 35 | } 36 | return mangledName 37 | } 38 | } 39 | 40 | internal func stdlib_demangleName( 41 | _ mangledName: UnsafePointer 42 | ) -> UnsafePointer { 43 | 44 | let demangledNamePtr = _stdlib_demangleImpl( 45 | mangledName: mangledName, 46 | mangledNameLength: numericCast(strlen(mangledName)), 47 | outputBuffer: nil, 48 | outputBufferSize: nil, 49 | flags: 0 50 | ) 51 | if let demangledNamePtr { 52 | return .init(demangledNamePtr) 53 | } 54 | return mangledName 55 | } 56 | -------------------------------------------------------------------------------- /Sources/RestrictCallCore/Extension/IndexStoreSymbol+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexStoreSymbol+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2024/06/14 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftIndexStore 11 | 12 | extension IndexStoreSymbol { 13 | var demangledName: String? { 14 | guard let usr else { return nil } 15 | 16 | // swift symbol 17 | if usr.hasPrefix("s:") { 18 | let index = usr.lastIndex(of: ":").map { usr.index($0, offsetBy: -1) } 19 | if let index { 20 | var symbol = String(usr[index...]) 21 | symbol.replaceSubrange(symbol.startIndex...symbol.index(symbol.startIndex, offsetBy: 1), with: "$S") 22 | return stdlib_demangleName(symbol) 23 | } 24 | } 25 | return stdlib_demangleName(usr) 26 | } 27 | } 28 | 29 | extension IndexStoreSymbol { 30 | public func matches( 31 | target: RestrictedTarget 32 | ) -> Bool { 33 | let demangledName = demangledName ?? name ?? usr ?? "" 34 | if demangledName.matches(pattern: target.demangledNamePattern) { 35 | return true 36 | } 37 | 38 | let prefix = [target.module, target.type].compactMap(\.self).joined(separator: ".") 39 | if let name, 40 | !prefix.isEmpty, 41 | demangledName.hasPrefix(prefix), 42 | name.matches(pattern: target.name) 43 | { 44 | return true 45 | } 46 | 47 | if let name, 48 | let targetModule = target.module, 49 | demangledName.hasPrefix("(extension in \(targetModule))"), 50 | name.matches(pattern: target.name) { 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-restrict-call", 7 | platforms: [ 8 | .macOS(.v13) 9 | ], 10 | products: [ 11 | .plugin( 12 | name: "RestrictCallBuildToolPlugin", 13 | targets: ["RestrictCallBuildToolPlugin"] 14 | ), 15 | .executable( 16 | name: "restrict-call", 17 | targets: ["restrict-call"] 18 | ) 19 | ], 20 | dependencies: [ 21 | .package( 22 | url: "https://github.com/apple/swift-argument-parser.git", 23 | from: "1.2.0" 24 | ), 25 | .package( 26 | url: "https://github.com/kateinoigakukun/swift-indexstore.git", 27 | from: "0.3.0" 28 | ), 29 | .package( 30 | url: "https://github.com/jpsim/Yams.git", 31 | from: "5.0.1" 32 | ), 33 | .package( 34 | url: "https://github.com/p-x9/swift-source-reporter.git", 35 | from: "0.2.0" 36 | ), 37 | ], 38 | targets: [ 39 | .executableTarget( 40 | name: "restrict-call", 41 | dependencies: [ 42 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 43 | .product(name: "SwiftIndexStore", package: "swift-indexstore"), 44 | .product(name: "Yams", package: "Yams"), 45 | "RestrictCallCore", 46 | ] 47 | ), 48 | .target( 49 | name: "RestrictCallCore", 50 | dependencies: [ 51 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 52 | .product(name: "SwiftIndexStore", package: "swift-indexstore"), 53 | .product(name: "SourceReporter", package: "swift-source-reporter"), 54 | ] 55 | ), 56 | .plugin( 57 | name: "RestrictCallBuildToolPlugin", 58 | capability: .buildTool(), 59 | dependencies: [ 60 | "restrict-call" 61 | ] 62 | ), 63 | ] 64 | ) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-restrict-call 2 | 3 | A Swift build tool plugin that reports and restricts calls to specific methods or properties in your Swift codebase. 4 | 5 | It is useful for enforcing architectural rules or preventing the use of certain APIs. 6 | 7 | It provides more rigorous detection than syntax-based analysis. 8 | 9 | ## Motivtion 10 | 11 | If you want to detect the use of URL.init(string:), the following usage patterns are possible, but it is impossible to detect all of them by syntax-based analysis. 12 | 13 | ```swift 14 | let url = URL.init(string: "") // Easily detectable even on a syntax basis 15 | let url = URL(string: "") // This is probably detectable as well. 16 | 17 | UIApplication.shared.open(.init(string: "")) // Syntax-based detection would be impossible for such a pattern. 18 | ``` 19 | 20 | The tool makes it possible to check exactly which method of which type is being called in which module by referring to the information in the IndexStore. 21 | 22 | ## Features 23 | 24 | - **Restrict method/property calls**: Detect and report usage of specified methods or properties. 25 | - **Customizable reporting**: Choose to report violations as errors or warnings. 26 | - **Configurable via YAML**: Define restricted targets and exclusions in a YAML config file. 27 | - **Build tool plugin**: Integrate with SwiftPM builds for automated checks. 28 | 29 | ## Requirements 30 | 31 | - macOS 13 or later 32 | - Swift 6.0 or later 33 | 34 | ## Usage 35 | 36 | ### Plugin 37 | 38 | Place the config file and configure the plugin as follows. 39 | This will result in a report at build time. 40 | 41 | Add the plugin to your `Package.swift`: 42 | 43 | ```swift 44 | .plugins([ 45 | .plugin(name: "RestrictCallBuildToolPlugin", package: "restrict-call") 46 | ]) 47 | ``` 48 | 49 | ### Command Line 50 | 51 | ```sh 52 | restrict-call --config .swift-restrict-call.yml --index-store-path 53 | ``` 54 | 55 | - `--config`: Path to the YAML config file (default: `.swift-restrict-call.yml`) 56 | - `--index-store-path`: Path to the IndexStore directory 57 | 58 | ### Configuration 59 | 60 | Create a `.swift-restrict-call.yml` file in your project root. 61 | Describe the name of the method or property whose invocation you want to restrict **using a regular expression**. 62 | 63 | > [!NOTE] 64 | > The method/property name check is based on the demangled symbol name. 65 | 66 | ```yaml 67 | defaultReportType: warning 68 | targets: 69 | - reportType: error 70 | module: "Foundation" 71 | type: "URL" 72 | name: "init\\(string:\\)" 73 | - reportType: error 74 | module: "Foundation" 75 | type: "URL" 76 | name: "path$" 77 | excludedFiles: 78 | - ".*/DerivedData/.*" 79 | - "Tests/*" 80 | ``` 81 | 82 | ## License 83 | 84 | swift-restrict-call is released under the MIT License. See [LICENSE](./LICENSE) 85 | -------------------------------------------------------------------------------- /Plugins/RestrictCallBuildToolPlugin/plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexStoreDebugBuildToolPlugin.swift 3 | // 4 | // 5 | // Created by p-x9 on 2025/08/09 6 | // 7 | // 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | @main 13 | struct RestrictCallBuildToolPlugin: BuildToolPlugin { 14 | func createBuildCommands(context: PackagePlugin.PluginContext, target: PackagePlugin.Target) async throws -> [PackagePlugin.Command] { 15 | createBuildCommands( 16 | packageDirectory: context.package.directoryURL, 17 | workingDirectory: context.pluginWorkDirectoryURL, 18 | tool: try context.tool(named: "restrict-call") 19 | ) 20 | } 21 | 22 | private func createBuildCommands( 23 | packageDirectory: URL, 24 | workingDirectory: URL, 25 | tool: PluginContext.Tool 26 | ) -> [Command] { 27 | let configuration = packageDirectory.firstConfigurationFileInParentDirectories() 28 | 29 | var arguments: [String] = [] 30 | 31 | if let configuration { 32 | arguments += [ 33 | "--config", configuration.path 34 | ] 35 | } 36 | 37 | return [ 38 | .buildCommand( 39 | displayName: "RestrictCallBuildToolPlugin", 40 | executable: tool.url, 41 | arguments: arguments 42 | ) 43 | ] 44 | } 45 | } 46 | 47 | #if canImport(XcodeProjectPlugin) 48 | import XcodeProjectPlugin 49 | 50 | extension RestrictCallBuildToolPlugin: XcodeBuildToolPlugin { 51 | func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { 52 | return createBuildCommands( 53 | packageDirectory: context.xcodeProject.directoryURL, 54 | workingDirectory: context.pluginWorkDirectoryURL, 55 | tool: try context.tool(named: "restrict-call") 56 | ) 57 | } 58 | } 59 | #endif 60 | 61 | // ref: https://github.com/realm/SwiftLint/blob/main/Plugins/SwiftLintPlugin/Path%2BHelpers.swift 62 | extension URL { 63 | func firstConfigurationFileInParentDirectories() -> URL? { 64 | let defaultConfigurationFileNames = [ 65 | ".swift-restrict-call.yml" 66 | ] 67 | let proposedDirectories = sequence( 68 | first: self, 69 | next: { path in 70 | guard path.pathComponents.count > 1 else { 71 | // Check we're not at the root of this filesystem, as `removingLastComponent()` 72 | // will continually return the root from itself. 73 | return nil 74 | } 75 | 76 | return path.deletingLastPathComponent() 77 | } 78 | ) 79 | 80 | for proposedDirectory in proposedDirectories { 81 | for fileName in defaultConfigurationFileNames { 82 | let potentialConfigurationFile = proposedDirectory.appending(path: fileName) 83 | if potentialConfigurationFile.isAccessible() { 84 | return potentialConfigurationFile 85 | } 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | /// Safe way to check if the file is accessible from within the current process sandbox. 92 | private func isAccessible() -> Bool { 93 | let result = path.withCString { pointer in 94 | access(pointer, R_OK) 95 | } 96 | 97 | return result == 0 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/restrict-call/restrict_call.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | import SwiftIndexStore 4 | import Yams 5 | import SourceReporter 6 | import RestrictCallCore 7 | 8 | @main 9 | struct restrict_call: ParsableCommand { 10 | static let configuration: CommandConfiguration = .init( 11 | commandName: "restrict-call", 12 | abstract: "Reports and restricts calls to specific methods/properties.", 13 | shouldDisplay: true, 14 | helpNames: [.long, .short] 15 | ) 16 | 17 | @Option(help: "Report as `error` or `warning` (default: warning)") 18 | var reportType: ReportType? 19 | 20 | @Option( 21 | help: "Config", 22 | completion: .file(extensions: ["yml", "yaml"]) 23 | ) 24 | var config: String = ".swift-restrict-call.yml" 25 | 26 | @Option( 27 | help: "Path for IndexStore", 28 | completion: .directory 29 | ) 30 | var indexStorePath: String? 31 | 32 | var targets: [RestrictedTarget] = [] 33 | var excludedFiles: [String] = [] 34 | var onlyModules: [String]? = nil 35 | var excludeModules: [String] = [] 36 | 37 | lazy var indexStore: IndexStore? = { 38 | if let indexStorePath = indexStorePath ?? environmentIndexStorePath, 39 | FileManager.default.fileExists(atPath: indexStorePath) { 40 | let url = URL(fileURLWithPath: indexStorePath) 41 | return try? .open(store: url, lib: .open()) 42 | } else { 43 | return nil 44 | } 45 | }() 46 | 47 | mutating func run() throws { 48 | guard let indexStore else { 49 | fatalError("No IndexStore found at specified path or in environment variable BUILD_DIR") 50 | } 51 | try readConfig() 52 | let reporter = RestrictCallReporter( 53 | defaultReportType: reportType ?? .warning, 54 | reporter: XcodeReporter(), 55 | targets: targets, 56 | excludedFiles: excludedFiles, 57 | onlyModules: onlyModules, 58 | excludeModules: excludeModules, 59 | indexStore: indexStore 60 | ) 61 | try reporter.run() 62 | } 63 | } 64 | 65 | @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) 66 | extension restrict_call: AsyncParsableCommand { 67 | mutating func run() async throws { 68 | guard let indexStore else { 69 | fatalError("No IndexStore found at specified path or in environment variable BUILD_DIR") 70 | } 71 | try readConfig() 72 | let reporter = RestrictCallReporter( 73 | defaultReportType: reportType ?? .warning, 74 | reporter: XcodeReporter(), 75 | targets: targets, 76 | excludedFiles: excludedFiles, 77 | onlyModules: onlyModules, 78 | excludeModules: excludeModules, 79 | indexStore: indexStore 80 | ) 81 | try await reporter.runConcurrently() 82 | } 83 | } 84 | 85 | extension restrict_call { 86 | private mutating func readConfig() throws { 87 | guard FileManager.default.fileExists(atPath: config) else { 88 | return 89 | } 90 | let url = URL(fileURLWithPath: config) 91 | let decoder = YAMLDecoder() 92 | 93 | let data = try Data(contentsOf: url) 94 | let config = try decoder.decode(Config.self, from: data) 95 | 96 | targets = config.targets 97 | excludedFiles = config.excludedFiles ?? [] 98 | onlyModules = config.onlyModules 99 | excludeModules = config.excludeModules ?? [] 100 | 101 | if reportType == nil { 102 | self.reportType = config.defaultReportType 103 | } 104 | } 105 | } 106 | 107 | extension restrict_call { 108 | var environmentIndexStorePath: String? { 109 | let environment = ProcessInfo.processInfo.environment 110 | guard let buildDir = environment["BUILD_DIR"] else { return nil } 111 | let url = URL(fileURLWithPath: buildDir) 112 | return url 113 | .deletingLastPathComponent() 114 | .deletingLastPathComponent() 115 | .appending(path: "Index.noindex/DataStore/") 116 | .path() 117 | } 118 | } 119 | 120 | extension XcodeReporter: @retroactive @unchecked Sendable {} 121 | -------------------------------------------------------------------------------- /Sources/RestrictCallCore/RestrictCallReporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestrictCallReporter.swift 3 | // swift-restrict-call 4 | // 5 | // Created by p-x9 on 2025/08/15 6 | // 7 | // 8 | 9 | import Foundation 10 | @preconcurrency import SwiftIndexStore 11 | import SourceReporter 12 | 13 | public final class RestrictCallReporter: Sendable { 14 | public let defaultReportType: ReportType 15 | public let reporter: any (ReporterProtocol & Sendable) 16 | 17 | public let targets: [RestrictedTarget] 18 | 19 | public let excludedFiles: [String] 20 | public let onlyModules: [String]? 21 | public let excludeModules: [String] 22 | 23 | public let indexStore: IndexStore 24 | 25 | public init( 26 | defaultReportType: ReportType, 27 | reporter: any (ReporterProtocol & Sendable), 28 | targets: [RestrictedTarget], 29 | excludedFiles: [String], 30 | onlyModules: [String]?, 31 | excludeModules: [String], 32 | indexStore: IndexStore 33 | ) { 34 | self.defaultReportType = defaultReportType 35 | self.reporter = reporter 36 | self.targets = targets 37 | self.excludedFiles = excludedFiles 38 | self.onlyModules = onlyModules 39 | self.excludeModules = excludeModules 40 | self.indexStore = indexStore 41 | } 42 | } 43 | 44 | extension RestrictCallReporter { 45 | public func run() throws { 46 | try indexStore.forEachUnits(includeSystem: false) { unit in 47 | guard try shouldReport(for: unit) else { return true } 48 | try indexStore.forEachRecordDependencies(for: unit) { dependency in 49 | guard case let .record(record) = dependency, 50 | shouldReport(for: record) else { 51 | return true 52 | } 53 | try indexStore.forEachOccurrences(for: record) { occurrence in 54 | reportIfNeeded(for: occurrence) 55 | return true 56 | } // forEachOccurrences 57 | return true 58 | } // forEachRecordDependencies 59 | return true 60 | } // forEachUnits 61 | } 62 | 63 | public func runConcurrently() async throws { 64 | let units = indexStore.units(includeSystem: false) 65 | 66 | try await units.concurrentForEach { unit in 67 | guard try self.shouldReport(for: unit) else { return } 68 | try self.indexStore.forEachRecordDependencies(for: unit) { dependency in 69 | guard case let .record(record) = dependency, 70 | self.shouldReport(for: record) else { 71 | return true 72 | } 73 | try self.indexStore.forEachOccurrences(for: record) { occurrence in 74 | self.reportIfNeeded(for: occurrence) 75 | return true 76 | } // forEachOccurrences 77 | return true 78 | } // forEachRecordDependencies 79 | } 80 | } 81 | } 82 | 83 | extension RestrictCallReporter { 84 | private func shouldReport(for unit: IndexStoreUnit) throws -> Bool { 85 | let path = try indexStore.mainFilePath(for: unit) 86 | let module = try indexStore.moduleName(for: unit) 87 | 88 | if excludedFiles.contains( 89 | where: { path?.matches(pattern: $0) ?? true } 90 | ) { 91 | return false 92 | } 93 | 94 | if let onlyModules { 95 | guard let module else { return false } 96 | if !onlyModules.contains(module) { 97 | return false 98 | } 99 | } 100 | 101 | if let module, excludeModules.contains(module) { 102 | return false 103 | } 104 | 105 | return true 106 | } 107 | 108 | private func shouldReport(for record: IndexStoreUnit.Dependency.Record) -> Bool { 109 | !excludedFiles.contains(where: { record.filePath?.matches(pattern: $0) ?? true }) 110 | } 111 | 112 | private func reportIfNeeded(for occurrence: IndexStoreOccurrence) { 113 | let symbol = occurrence.symbol 114 | 115 | for target in self.targets { 116 | let roles = occurrence.roles 117 | guard symbol.matches(target: target), 118 | roles.contains([.reference, .call]) else { 119 | continue 120 | } 121 | reporter.report( 122 | file: occurrence.location.path, 123 | line: numericCast(occurrence.location.line), 124 | column: numericCast(occurrence.location.column), 125 | type: target.reportType ?? defaultReportType, 126 | content: "[restrict-call] `\(target.demangledNamePattern)` calls are restricted." 127 | ) 128 | break 129 | } 130 | } 131 | } 132 | 133 | extension Array { 134 | func concurrentForEach( 135 | _ body: @escaping @Sendable (Element) async throws -> Void 136 | ) async throws where Element: Sendable { 137 | try await withThrowingTaskGroup(of: Void.self) { group in 138 | for element in self { 139 | group.addTask { 140 | try await body(element) 141 | } 142 | } 143 | try await group.waitForAll() 144 | } 145 | } 146 | } 147 | --------------------------------------------------------------------------------