├── .github └── workflows │ └── test.yml ├── .gitignore ├── IntegrationTests ├── IntegrationTests.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Sources │ ├── ViewController.swift │ └── ViewModel.swift └── project.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftRenamer │ ├── SourceRewriter.swift │ └── SwiftRenamer.swift └── Tests ├── LinuxMain.swift └── SwiftRenamerTests ├── IntegrationTests.swift ├── SourceRewriterTests.swift └── XCTestManifests.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and run tests 2 | on: 3 | push: {} 4 | pull_request: {} 5 | jobs: 6 | run: 7 | runs-on: macOS-latest 8 | name: Build and run tests 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Xcode version 12 | run: /usr/bin/xcode-select --print-path 13 | - name: Build 14 | run: swift build 15 | - name: Test 16 | run: swift test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4B8ECE23351BFB776850040E70A89A02 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA98229F40C764556C94ED5138BD9E10 /* ViewModel.swift */; }; 11 | D48625A13389AE0D7D6751FFB1AA5FD7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60CD895AD4F787BDF9DC4A2FE2940B7 /* ViewController.swift */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 54F0D655DA305AB994A574D2B5372A24 /* IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | B60CD895AD4F787BDF9DC4A2FE2940B7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 17 | FA98229F40C764556C94ED5138BD9E10 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 18 | /* End PBXFileReference section */ 19 | 20 | /* Begin PBXGroup section */ 21 | 1D9327473CB1F47F5755F0F32E4606B0 = { 22 | isa = PBXGroup; 23 | children = ( 24 | 2E986E13CAC17BA147D51A1E38FB193E /* Sources */, 25 | 9D61F26C168B587B004F546979D0AB23 /* Products */, 26 | ); 27 | sourceTree = ""; 28 | }; 29 | 2E986E13CAC17BA147D51A1E38FB193E /* Sources */ = { 30 | isa = PBXGroup; 31 | children = ( 32 | B60CD895AD4F787BDF9DC4A2FE2940B7 /* ViewController.swift */, 33 | FA98229F40C764556C94ED5138BD9E10 /* ViewModel.swift */, 34 | ); 35 | path = Sources; 36 | sourceTree = ""; 37 | }; 38 | 9D61F26C168B587B004F546979D0AB23 /* Products */ = { 39 | isa = PBXGroup; 40 | children = ( 41 | 54F0D655DA305AB994A574D2B5372A24 /* IntegrationTests.framework */, 42 | ); 43 | name = Products; 44 | sourceTree = ""; 45 | }; 46 | /* End PBXGroup section */ 47 | 48 | /* Begin PBXNativeTarget section */ 49 | 8187FC35C0BE43AB4A5C850E6E227A9A /* IntegrationTests */ = { 50 | isa = PBXNativeTarget; 51 | buildConfigurationList = 8D8BA82B02357E83E1B89FC0696EDA39 /* Build configuration list for PBXNativeTarget "IntegrationTests" */; 52 | buildPhases = ( 53 | D3B979FB3279D2B1AE1E4FA15387A4FD /* Sources */, 54 | ); 55 | buildRules = ( 56 | ); 57 | dependencies = ( 58 | ); 59 | name = IntegrationTests; 60 | productName = IntegrationTests; 61 | productReference = 54F0D655DA305AB994A574D2B5372A24 /* IntegrationTests.framework */; 62 | productType = "com.apple.product-type.framework"; 63 | }; 64 | /* End PBXNativeTarget section */ 65 | 66 | /* Begin PBXProject section */ 67 | CC618D17E72AC0CFB2A7E7EFD0B0E332 /* Project object */ = { 68 | isa = PBXProject; 69 | attributes = { 70 | LastUpgradeCheck = 1020; 71 | }; 72 | buildConfigurationList = 6F34A9539E6805442595B706B3E0C59E /* Build configuration list for PBXProject "IntegrationTests" */; 73 | compatibilityVersion = "Xcode 9.3"; 74 | developmentRegion = en; 75 | hasScannedForEncodings = 0; 76 | knownRegions = ( 77 | en, 78 | ); 79 | mainGroup = 1D9327473CB1F47F5755F0F32E4606B0; 80 | projectDirPath = ""; 81 | projectRoot = ""; 82 | targets = ( 83 | 8187FC35C0BE43AB4A5C850E6E227A9A /* IntegrationTests */, 84 | ); 85 | }; 86 | /* End PBXProject section */ 87 | 88 | /* Begin PBXSourcesBuildPhase section */ 89 | D3B979FB3279D2B1AE1E4FA15387A4FD /* Sources */ = { 90 | isa = PBXSourcesBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | D48625A13389AE0D7D6751FFB1AA5FD7 /* ViewController.swift in Sources */, 94 | 4B8ECE23351BFB776850040E70A89A02 /* ViewModel.swift in Sources */, 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | /* End PBXSourcesBuildPhase section */ 99 | 100 | /* Begin XCBuildConfiguration section */ 101 | 608DC6F734EF1C507951649B32403363 /* Debug */ = { 102 | isa = XCBuildConfiguration; 103 | buildSettings = { 104 | CODE_SIGN_IDENTITY = ""; 105 | CURRENT_PROJECT_VERSION = 1; 106 | DEFINES_MODULE = YES; 107 | DYLIB_COMPATIBILITY_VERSION = 1; 108 | DYLIB_CURRENT_VERSION = 1; 109 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 110 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 111 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 112 | SDKROOT = iphoneos; 113 | SKIP_INSTALL = YES; 114 | TARGETED_DEVICE_FAMILY = "1,2"; 115 | VERSIONING_SYSTEM = "apple-generic"; 116 | }; 117 | name = Debug; 118 | }; 119 | 9601F69894841DDAC80B067099D253E8 /* Debug */ = { 120 | isa = XCBuildConfiguration; 121 | buildSettings = { 122 | ALWAYS_SEARCH_USER_PATHS = NO; 123 | CLANG_ANALYZER_NONNULL = YES; 124 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 125 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 126 | CLANG_CXX_LIBRARY = "libc++"; 127 | CLANG_ENABLE_MODULES = YES; 128 | CLANG_ENABLE_OBJC_ARC = YES; 129 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 130 | CLANG_WARN_BOOL_CONVERSION = YES; 131 | CLANG_WARN_COMMA = YES; 132 | CLANG_WARN_CONSTANT_CONVERSION = YES; 133 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 134 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 135 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 136 | CLANG_WARN_EMPTY_BODY = YES; 137 | CLANG_WARN_ENUM_CONVERSION = YES; 138 | CLANG_WARN_INFINITE_RECURSION = YES; 139 | CLANG_WARN_INT_CONVERSION = YES; 140 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 141 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 142 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 143 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 144 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 145 | CLANG_WARN_STRICT_PROTOTYPES = YES; 146 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 147 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 148 | CLANG_WARN_UNREACHABLE_CODE = YES; 149 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 150 | COPY_PHASE_STRIP = NO; 151 | DEBUG_INFORMATION_FORMAT = dwarf; 152 | ENABLE_STRICT_OBJC_MSGSEND = YES; 153 | ENABLE_TESTABILITY = YES; 154 | GCC_C_LANGUAGE_STANDARD = gnu11; 155 | GCC_DYNAMIC_NO_PIC = NO; 156 | GCC_NO_COMMON_BLOCKS = YES; 157 | GCC_OPTIMIZATION_LEVEL = 0; 158 | GCC_PREPROCESSOR_DEFINITIONS = ( 159 | "$(inherited)", 160 | "DEBUG=1", 161 | ); 162 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 163 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 164 | GCC_WARN_UNDECLARED_SELECTOR = YES; 165 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 166 | GCC_WARN_UNUSED_FUNCTION = YES; 167 | GCC_WARN_UNUSED_VARIABLE = YES; 168 | MTL_ENABLE_DEBUG_INFO = YES; 169 | ONLY_ACTIVE_ARCH = YES; 170 | PRODUCT_NAME = "$(TARGET_NAME)"; 171 | SDKROOT = iphoneos; 172 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 173 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 174 | SWIFT_VERSION = 5.0; 175 | }; 176 | name = Debug; 177 | }; 178 | CF04C5B138E80991342626D06AC937E0 /* Release */ = { 179 | isa = XCBuildConfiguration; 180 | buildSettings = { 181 | ALWAYS_SEARCH_USER_PATHS = NO; 182 | CLANG_ANALYZER_NONNULL = YES; 183 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 184 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 185 | CLANG_CXX_LIBRARY = "libc++"; 186 | CLANG_ENABLE_MODULES = YES; 187 | CLANG_ENABLE_OBJC_ARC = YES; 188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 189 | CLANG_WARN_BOOL_CONVERSION = YES; 190 | CLANG_WARN_COMMA = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INFINITE_RECURSION = YES; 198 | CLANG_WARN_INT_CONVERSION = YES; 199 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 204 | CLANG_WARN_STRICT_PROTOTYPES = YES; 205 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 206 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 207 | CLANG_WARN_UNREACHABLE_CODE = YES; 208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 209 | COPY_PHASE_STRIP = NO; 210 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 211 | ENABLE_NS_ASSERTIONS = NO; 212 | ENABLE_STRICT_OBJC_MSGSEND = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu11; 214 | GCC_NO_COMMON_BLOCKS = YES; 215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 217 | GCC_WARN_UNDECLARED_SELECTOR = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 219 | GCC_WARN_UNUSED_FUNCTION = YES; 220 | GCC_WARN_UNUSED_VARIABLE = YES; 221 | PRODUCT_NAME = "$(TARGET_NAME)"; 222 | SDKROOT = iphoneos; 223 | SWIFT_COMPILATION_MODE = wholemodule; 224 | SWIFT_VERSION = 5.0; 225 | VALIDATE_PRODUCT = YES; 226 | }; 227 | name = Release; 228 | }; 229 | F952ECC3BEC30552CE72F3E0DE75E4DD /* Release */ = { 230 | isa = XCBuildConfiguration; 231 | buildSettings = { 232 | CODE_SIGN_IDENTITY = ""; 233 | CURRENT_PROJECT_VERSION = 1; 234 | DEFINES_MODULE = YES; 235 | DYLIB_COMPATIBILITY_VERSION = 1; 236 | DYLIB_CURRENT_VERSION = 1; 237 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 238 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 239 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 240 | SDKROOT = iphoneos; 241 | SKIP_INSTALL = YES; 242 | TARGETED_DEVICE_FAMILY = "1,2"; 243 | VERSIONING_SYSTEM = "apple-generic"; 244 | }; 245 | name = Release; 246 | }; 247 | /* End XCBuildConfiguration section */ 248 | 249 | /* Begin XCConfigurationList section */ 250 | 6F34A9539E6805442595B706B3E0C59E /* Build configuration list for PBXProject "IntegrationTests" */ = { 251 | isa = XCConfigurationList; 252 | buildConfigurations = ( 253 | 9601F69894841DDAC80B067099D253E8 /* Debug */, 254 | CF04C5B138E80991342626D06AC937E0 /* Release */, 255 | ); 256 | defaultConfigurationIsVisible = 0; 257 | defaultConfigurationName = Debug; 258 | }; 259 | 8D8BA82B02357E83E1B89FC0696EDA39 /* Build configuration list for PBXNativeTarget "IntegrationTests" */ = { 260 | isa = XCConfigurationList; 261 | buildConfigurations = ( 262 | 608DC6F734EF1C507951649B32403363 /* Debug */, 263 | F952ECC3BEC30552CE72F3E0DE75E4DD /* Release */, 264 | ); 265 | defaultConfigurationIsVisible = 0; 266 | defaultConfigurationName = ""; 267 | }; 268 | /* End XCConfigurationList section */ 269 | }; 270 | rootObject = CC618D17E72AC0CFB2A7E7EFD0B0E332 /* Project object */; 271 | } 272 | -------------------------------------------------------------------------------- /IntegrationTests/IntegrationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IntegrationTests/IntegrationTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /IntegrationTests/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController { 4 | 5 | let viewModel = ViewModel() 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | viewModel.name = "Initial Name" 10 | viewModel.foo(input: 1) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /IntegrationTests/Sources/ViewModel.swift: -------------------------------------------------------------------------------- 1 | class ViewModel { 2 | typealias Input = Int 3 | func foo(input: Input) {} 4 | var name: String? 5 | } 6 | -------------------------------------------------------------------------------- /IntegrationTests/project.yml: -------------------------------------------------------------------------------- 1 | name: IntegrationTests 2 | targets: 3 | IntegrationTests: 4 | platform: iOS 5 | type: framework 6 | sources: 7 | - Sources 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yuta Saito 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 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 7 | "state": { 8 | "branch": null, 9 | "revision": "9f04d1ff1afbccd02279338a2c91e5f27c45e93a", 10 | "version": "0.0.5" 11 | } 12 | }, 13 | { 14 | "package": "SwiftIndexStore", 15 | "repositoryURL": "https://github.com/kateinoigakukun/swift-indexstore", 16 | "state": { 17 | "branch": null, 18 | "revision": "7e9bcef858509b62cf3a23bada0bc107cec0624f", 19 | "version": "0.1.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftRenamer", 7 | platforms: [.macOS(.v10_12)], 8 | products: [ 9 | .library( 10 | name: "SwiftRenamer", 11 | targets: ["SwiftRenamer"]) 12 | ], 13 | dependencies: [ 14 | .package(name: "SwiftIndexStore", url: "https://github.com/kateinoigakukun/swift-indexstore", .exact("0.1.0")), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "SwiftRenamer", 19 | dependencies: [ 20 | .product(name: "SwiftIndexStore", package: "SwiftIndexStore"), 21 | ]), 22 | .testTarget( 23 | name: "SwiftRenamerTests", 24 | dependencies: ["SwiftRenamer"]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-renamer 2 | 3 | SwiftRenamer is a tool that renames Swift identifiers 4 | 5 | 6 | If you want to rename `name` to `nickname`, 7 | ```swift 8 | class User { 9 | var name: String? 10 | } 11 | 12 | class ViewController: UIViewController { 13 | 14 | let user = User() 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | user.name = "Initial Name" 19 | } 20 | } 21 | ``` 22 | 23 | You can use this library like below: 24 | 25 | ```swift 26 | import SwiftRenamer 27 | 28 | let renamer = SwiftRenamer(storePath: indexStorePath) 29 | let replacements = try system.replacements(where: { (occ) -> String? in 30 | occ.symbol.usr == "s:16IntegrationTests9ViewModelC4nameSSSgvp" ? "nickname" : nil 31 | }) 32 | 33 | for (filePath, replacements) in replacements { 34 | let rewriter = try SourceRewriter(content: String(contentsOfFile: filePath)) 35 | replacements.forEach(rewriter.replace) 36 | let newContent = rewriter.apply() 37 | newContent.write(toFile: filePath, atomically: true, encoding: .utf8) 38 | } 39 | ``` 40 | 41 | Then, the file will be rewritten as: 42 | 43 | ```diff 44 | class User { 45 | - var name: String? 46 | + var nickname: String? 47 | } 48 | 49 | class ViewController: UIViewController { 50 | 51 | let user = User() 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | 55 | - user.name = "Initial Name" 56 | + user.nickname = "Initial Name" 57 | } 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /Sources/SwiftRenamer/SourceRewriter.swift: -------------------------------------------------------------------------------- 1 | public struct Replacement: Equatable { 2 | 3 | struct Location: Hashable { 4 | let line: Int64 5 | let column: Int64 6 | } 7 | 8 | let location: Location 9 | let length: Int 10 | let newText: String 11 | } 12 | 13 | public class SourceRewriter { 14 | let content: String 15 | 16 | var replacements: [Replacement] = [] 17 | 18 | public init(content: String) { 19 | self.content = content 20 | } 21 | 22 | public func replace(_ replacement: Replacement) { 23 | replacements.append(replacement) 24 | } 25 | 26 | public func apply() -> String { 27 | let sorted = replacements.unique().sorted(by: { 28 | $0.location.line == $1.location.line ? 29 | $0.location.column > $1.location.column : 30 | $0.location.line < $1.location.line 31 | }) 32 | var lines = content.components(separatedBy: "\n") 33 | 34 | for replacement in sorted { 35 | 36 | let lineIndex = Int(replacement.location.line) - 1 37 | var line = lines[lineIndex] 38 | 39 | let startOffset = Int(replacement.location.column - 1) 40 | let startIndex = line.index(line.startIndex, offsetBy: startOffset) 41 | let endIndex = line.index(startIndex, offsetBy: Int(replacement.length)) 42 | line.replaceSubrange(startIndex.. [Element] { 53 | reduce([Element]()) { $0.contains($1) ? $0 : $0 + [$1] } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/SwiftRenamer/SwiftRenamer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftIndexStore 3 | 4 | public struct SwiftRenamer { 5 | 6 | let indexStore: IndexStore 7 | public init(indexStore: IndexStore) { 8 | self.indexStore = indexStore 9 | } 10 | 11 | public init(storePath: URL) throws { 12 | self.indexStore = try IndexStore.open(store: storePath, lib: .open()) 13 | } 14 | 15 | public func replacements(where condition: (IndexStoreOccurrence) -> String?) throws -> [String: [Replacement]] { 16 | 17 | var usrToOccurrence: [String: [IndexStoreOccurrence]] = [:] 18 | var entries: [(usr: String, newText: String)] = [] 19 | 20 | try self.indexStore.forEachUnits { unit -> Bool in 21 | try self.indexStore.forEachRecordDependencies(for: unit) { dependency in 22 | guard case let .record(record) = dependency else { return true } 23 | try self.indexStore.forEachOccurrences(for: record) { (occurrence) -> Bool in 24 | guard let usr = occurrence.symbol.usr, 25 | !occurrence.location.isSystem else { return true } 26 | 27 | if usrToOccurrence[usr] == nil { 28 | usrToOccurrence[usr] = [occurrence] 29 | } else { 30 | usrToOccurrence[usr]?.append(occurrence) 31 | } 32 | 33 | guard let newSymbol = condition(occurrence) else { return true } 34 | entries.append((usr, newSymbol)) 35 | 36 | return true 37 | } 38 | return true 39 | } 40 | return true 41 | } 42 | 43 | var results: [/* path */ String: [Replacement]] = [:] 44 | 45 | for entry in entries { 46 | let occs = usrToOccurrence[entry.usr]! 47 | for occ in occs { 48 | guard let symbolName = occ.symbol.name, 49 | let occPath = occ.location.path else { continue } 50 | let symbolLength: Int 51 | 52 | if occ.symbol.kind == .instanceMethod { 53 | guard let indexOfLastOfName = symbolName.firstIndex(of: "(") else { continue } 54 | symbolLength = symbolName.distance(from: symbolName.startIndex, to: indexOfLastOfName) 55 | } else { 56 | symbolLength = symbolName.count 57 | } 58 | 59 | let replacement = Replacement( 60 | location: .init(line: occ.location.line, column: occ.location.column), 61 | length: symbolLength, newText: entry.newText 62 | ) 63 | 64 | if results[occPath] == nil { 65 | results[occPath] = [replacement] 66 | } else { 67 | results[occPath]?.append(replacement) 68 | } 69 | } 70 | } 71 | return results 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftRenameTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftRenameTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftRenamerTests/IntegrationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftRenamer 3 | 4 | class IntegrationTests: XCTestCase { 5 | 6 | static var indexStorePath: URL! 7 | static let projectPath = URL(fileURLWithPath: #file) 8 | .deletingLastPathComponent() 9 | .deletingLastPathComponent() 10 | .deletingLastPathComponent() 11 | .appendingPathComponent("IntegrationTests") 12 | 13 | override class func setUp() { 14 | let derivedData = FileManager.default.temporaryDirectory 15 | .appendingPathComponent("DerivedData-\(UUID())") 16 | 17 | let process = Process() 18 | process.currentDirectoryPath = projectPath.path 19 | process.launchPath = "/usr/bin/xcodebuild" 20 | process.arguments = ["build", "-scheme", "IntegrationTests", "-derivedDataPath", derivedData.path] 21 | process.launch() 22 | process.waitUntilExit() 23 | 24 | indexStorePath = derivedData 25 | .appendingPathComponent("Index") 26 | .appendingPathComponent("DataStore") 27 | } 28 | 29 | func rewrite(replacements: [String: [Replacement]]) throws -> [String: String] { 30 | return try replacements.reduce(into: [String: String]()) { (result, element) throws -> Void in 31 | let (path, replacements) = element 32 | let rewriter = try SourceRewriter(content: String(contentsOfFile: path)) 33 | replacements.forEach(rewriter.replace) 34 | result[String(path.split(separator: "/").last!)] = rewriter.apply() 35 | } 36 | } 37 | 38 | func testRewrite() throws { 39 | let system = try SwiftRenamer(storePath: Self.indexStorePath) 40 | 41 | let replacements = try system.replacements(where: { (occ) -> String? in 42 | if occ.symbol.usr == "s:16IntegrationTests9ViewModelC4nameSSSgvp" { 43 | return "nickname" 44 | } 45 | if occ.symbol.name == "foo(input:)", occ.symbol.kind == .instanceMethod { 46 | return "bar" 47 | } 48 | return nil 49 | }) 50 | 51 | let results = try rewrite(replacements: replacements) 52 | 53 | XCTAssertEqual(results["ViewModel.swift"], """ 54 | class ViewModel { 55 | typealias Input = Int 56 | func bar(input: Input) {} 57 | var nickname: String? 58 | } 59 | 60 | """) 61 | 62 | XCTAssertEqual(results["ViewController.swift"], """ 63 | import UIKit 64 | 65 | class ViewController: UIViewController { 66 | 67 | let viewModel = ViewModel() 68 | override func viewDidLoad() { 69 | super.viewDidLoad() 70 | 71 | viewModel.nickname = "Initial Name" 72 | viewModel.bar(input: 1) 73 | } 74 | } 75 | 76 | """) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/SwiftRenamerTests/SourceRewriterTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftRenamer 3 | 4 | class SourceRewiterTests: XCTestCase { 5 | 6 | func testSingleReplacement() { 7 | let content = """ 8 | foo bar fizz 9 | """ 10 | 11 | let rewriter = SourceRewriter(content: content) 12 | let replacement = Replacement( 13 | location: .init(line: 1, column: 5), length: 3, newText: "buzz" 14 | ) 15 | rewriter.replace(replacement) 16 | let result = rewriter.apply() 17 | 18 | XCTAssertEqual(result, """ 19 | foo buzz fizz 20 | """) 21 | } 22 | 23 | func testMultipleReplacement() { 24 | let content = """ 25 | foo bar fizz 26 | Hello, new world 27 | """ 28 | 29 | let replacement1 = Replacement( 30 | location: .init(line: 1, column: 5), length: 3, newText: "buzz" 31 | ) 32 | let replacement2 = Replacement( 33 | location: .init(line: 2, column: 8), length: 3, newText: "old" 34 | ) 35 | let replacements = [replacement1, replacement2] 36 | 37 | let rewriter = SourceRewriter(content: content) 38 | replacements.forEach(rewriter.replace) 39 | let result = rewriter.apply() 40 | 41 | XCTAssertEqual(result, """ 42 | foo buzz fizz 43 | Hello, old world 44 | """) 45 | } 46 | 47 | func testMultipleReplacementInSameLine() { 48 | let content = """ 49 | Hello, new world 50 | """ 51 | 52 | let replacement1 = Replacement( 53 | location: .init(line: 1, column: 8), length: 3, newText: "old" 54 | ) 55 | let replacement2 = Replacement( 56 | location: .init(line: 1, column: 1), length: 5, newText: "Good bye" 57 | ) 58 | let replacements = [replacement1, replacement2] 59 | 60 | let rewriter = SourceRewriter(content: content) 61 | replacements.forEach(rewriter.replace) 62 | let result = rewriter.apply() 63 | 64 | XCTAssertEqual(result, """ 65 | Good bye, old world 66 | """) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/SwiftRenamerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftRenameTests.allTests) 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------