Skip to content

Commit

Permalink
Support for Generics (ex: AnyPublisher), Closures, Computed variable,…
Browse files Browse the repository at this point in the history
… Function return clause, Function body
  • Loading branch information
paul1893 committed Feb 19, 2024
1 parent 2b72dae commit 6cff2f1
Show file tree
Hide file tree
Showing 18 changed files with 1,075 additions and 403 deletions.
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
BUILD_PATH=.build
SWIFT_BUILD_FLAGS=--product CommandLineTool --configuration release --disable-sandbox --scratch-path ${BUILD_PATH}

EXECUTABLE_X86_64=$(shell swift build ${SWIFT_BUILD_FLAGS} --arch x86_64 --show-bin-path)/CommandLineTool
EXECUTABLE_ARM64=$(shell swift build ${SWIFT_BUILD_FLAGS} --arch arm64 --show-bin-path)/CommandLineTool
EXECUTABLE=${BUILD_PATH}/CommandLineTool

clean:
@swift package clean

build_x86_64:
@swift build ${SWIFT_BUILD_FLAGS} --arch x86_64

build_arm64:
@swift build ${SWIFT_BUILD_FLAGS} --arch arm64

build_release: clean build_x86_64 build_arm64
@lipo -create -output ${EXECUTABLE} ${EXECUTABLE_X86_64} ${EXECUTABLE_ARM64}
@strip -rSTX ${EXECUTABLE}

show_bin_path:
@echo ${EXECUTABLE}
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ let package = Package(
],
targets: [
.executableTarget(
name: "SwiftSomeAnyMigratorCommandLineTool",
name: "CommandLineTool",
dependencies: ["SwiftSomeAnyMigrator"],
path: "Sources/SwiftSomeAnyMigratorCommandLineTool"
path: "Sources/CommandLineTool"
),
.target(
name: "SwiftSomeAnyMigrator",
Expand All @@ -31,7 +31,7 @@ let package = Package(
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
path: "Sources/SwiftSomeAnyMigrator"
path: "Sources/Command"
),
.testTarget(
name: "SwiftSomeAnyMigratorTests",
Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@
</h1>

# SwiftSomeAnyMigrator
A tool to help you migrate your Swift code to new any and some keywords.
A tool to help you migrate your Swift code to new `any` and `some` keywords.

# How to use
1. Download the release on Github
2. Run

```swift
SwiftSomeAnyMigratorCommandLineTool ./your/path/folder
// From a bash script
migrator ./your/path/folder
--ignoreFolders "FooFolder, BarFolder" \
--policy strict \
--conservative
```

```swift
// From yout terminal
./migrator ./your/path/folder --conservative
```

# How it works
SwiftSomeAnyMigrator will found all the Swift files recursively for a given path and will add `some` or `any` based on your policy.

<h1 align="center">
<img src="Screenshots/demo.gif" alt="SwiftSomeAnyMigrator" width="800" />
<br>
</h1>

## Policy option
You can use `--policy strict` or `--policy light`.
`light` use `any` everywhere while `strict` will try to introduce `some` keywords whenever think it's possible.
Expand Down Expand Up @@ -69,12 +80,12 @@ If you add `--conservative` flag, the tool will keep pre-existing `any` or `some

# F.A.Q
### Why is my code does not compile after running a `strict` migration ?
SwiftSomeAnyMigrator is only making static analysis he could not understand what is really injected and can only make syntax guess. Because of this, his guess could be wrong sometimes. It does NOT mean you should always use `light` mode. SwiftSomeAnyMigrator is an helper tool to cover a maximum of cases, resulting to the developer to only arbitrate a bunch of cases after the tool have runned instead of the whole codebase.
SwiftSomeAnyMigrator is only making static analysis, he could not understand what is really injected and can only make syntax guess. Because of this, his guess could be wrong sometimes. It does NOT mean you should always use `light` mode. SwiftSomeAnyMigrator is an helper tool to cover a maximum of cases, resulting to the developer to only arbitrate a bunch of cases after the tool have runned instead of the whole codebase.

<h1 align="center">
<img src="Screenshots/static-analysis.png" alt="static-analysis" height="320" />
</h1>

### What about performance ?
SwiftSomeAnyMigrator uses SwiftConcurrency behind the hood to apply changes concurrently and faster to your codebase.
6.2 sec. for 1k files.
SwiftSomeAnyMigrator uses SwiftConcurrency behind the hood to apply changes concurrently and faster to your codebase.
~3 sec. / 1k files.
Binary file added Screenshots/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,7 @@ import Foundation
import ArgumentParser
import SwiftParser

enum Metadata {
static var policy = Policy.strict
static var conservative = false
}

public enum Policy: String, ExpressibleByArgument {
case strict, light

public init?(argument: String) {
if let policy = Policy(rawValue: argument) {
self = policy
} else {
return nil
}
}
}
public struct SwiftSomeAnyMigratorCommand: AsyncParsableCommand {
public struct Command: AsyncParsableCommand {
public static let configuration = CommandConfiguration(
abstract: "A Swift command-line tool to add `any` and/or `some` keywords at needed locations in your codebase for Swift 6 compatibility",
subcommands: []
Expand Down Expand Up @@ -55,23 +39,67 @@ public struct SwiftSomeAnyMigratorCommand: AsyncParsableCommand {
ignoringFolders: ignoreFolders
)
}
}

extension Command {
private func processDirectory(at path: String, ignoringFolders: [String]) async {
print("📦 Preparing ...")
let directoryURL = URL(fileURLWithPath: path)

print("📦 Collecting protocol types ...")
await iterate(through: directoryURL, ignoringFolders: ignoringFolders) { fileURL in
let sourceText = try String(contentsOf: fileURL)
let sourceFile = Parser.parse(source: sourceText)
let visitor = ProtocolVisitor(viewMode: .sourceAccurate)
visitor.walk(sourceFile)
}

print("📦 Migration ...")
var start: CFAbsoluteTime?
await iterate(
through: directoryURL,
ignoringFolders: ignoringFolders,
willStart: {
if start == nil {
start = CFAbsoluteTimeGetCurrent()
}
}) { fileURL in
print("✅ Updating \(fileURL.lastPathComponent)")
let sourceText = try String(contentsOf: fileURL)
let sourceFile = Parser.parse(source: sourceText)

let modifiedSource = MyRewriter().visit(sourceFile)

try modifiedSource
.description
.write(to: fileURL, atomically: true, encoding: .utf8)
}
if let start {
let end = CFAbsoluteTimeGetCurrent()
print("✅ Complete in \(end - start) sec.")
}
}

private func iterate(
through directoryURL: URL,
ignoringFolders: [String],
willStart: (() -> Void)? = nil,
perform: @escaping (URL) throws -> Void
) async {
let fileManager = FileManager.default
let options: FileManager.DirectoryEnumerationOptions = [
.skipsHiddenFiles,
.skipsPackageDescendants
]

print("📦 Looking for files ...")
var start: CFAbsoluteTime?

await withThrowingTaskGroup(of: Void.self) { group in
if let enumerator = fileManager.enumerator(at: directoryURL, includingPropertiesForKeys: nil, options: options) {
if let enumerator = fileManager.enumerator(
at: directoryURL,
includingPropertiesForKeys: nil,
options: options
) {
willStart?()
for case let fileURL as URL in enumerator {
if start == nil { start = CFAbsoluteTimeGetCurrent() }
group.addTask {
do {
if fileURL.hasDirectoryPath {
Expand All @@ -80,39 +108,25 @@ public struct SwiftSomeAnyMigratorCommand: AsyncParsableCommand {
let resourceValues = try fileURL.resourceValues(forKeys: [.isDirectoryKey])
if resourceValues.isDirectory == true {
if ignoringFolders.contains(fileURL.lastPathComponent) {
enumerator.skipDescendants() // Skip this directory and its subdirectories
// Skip this directory and its subdirectories
enumerator.skipDescendants()
return
}
}
// Check if the file is a Swift file
try Task.checkCancellation()
if fileURL.path.hasSuffix(".swift") {
print("✅ Updating \(fileURL.lastPathComponent)")
let sourceText = try String(contentsOf: fileURL)
let sourceFile = Parser.parse(source: sourceText)

var modifiedSource = sourceFile
let variableRewriter = GlobalVariableProtocolRewriter()
modifiedSource = variableRewriter.visit(modifiedSource)
let constructorRewriter = InitializerProtocolRewriter()
modifiedSource = constructorRewriter.visit(modifiedSource)

try modifiedSource
.description
.write(to: fileURL, atomically: true, encoding: .utf8)
try perform(fileURL)
}
} catch {
print("Error reading contents of directory \(fileURL.path): \(error)")
print(
"Error reading contents of directory \(fileURL.path): \(error)"
)
return
}
}
}
}
}
if let start {
let end = CFAbsoluteTimeGetCurrent()
print("✅ Complete in \(end - start) sec.")
}
}

}
27 changes: 27 additions & 0 deletions Sources/Command/Extensions/Naming+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftSyntax

extension InitializerClauseSyntax {
var hasDefaultValueWithSingleton: Bool {
trimmedDescription.contains(".shared.") == true
}
}

extension TokenSyntax {
var isProtocol: Bool {
self.text.isProtocolName
}
}

extension TypeSyntax {
var isProtocol: Bool {
self.trimmedDescription.isProtocolName
}
}

extension String {
var isProtocolName: Bool {
return Collector.protocols.contains(where: {
$0.trimmedDescription == self
})
}
}
Loading

0 comments on commit 6cff2f1

Please sign in to comment.