From b5d3c5107838cda67eb5bdffcd085fcbb40048cf Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 20 Mar 2026 03:43:32 +0900 Subject: [PATCH 1/6] Replace SourceKitten with SwiftSyntax implementation Co-authored-by: Wataru343 <20026379+Wataru343@users.noreply.github.com> --- Package.resolved | 26 +- Package.swift | 12 +- .../Commands/CreateComponentExtension.swift | 1 - .../Commands/CreateRIBCommand.swift | 1 - .../Commands/DeleteRIBCommand.swift | 3 - .../Commands/DependencyCommand.swift | 6 +- .../RIBsCodeGen/Commands/RenameCommand.swift | 1 - .../RIBsCodeGen/Commands/UnlinkCommand.swift | 1 - .../Extensions/Array+SourceKit.swift | 81 +- .../Extensions/Dictionary+SourceKit.swift | 84 +-- Sources/RIBsCodeGen/Models/Formatter.swift | 1 - .../Models/SwiftSyntaxSupport.swift | 712 ++++++++++++++++++ Sources/RIBsCodeGen/main.swift | 3 +- 13 files changed, 779 insertions(+), 153 deletions(-) create mode 100644 Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift diff --git a/Package.resolved b/Package.resolved index 14ce089..8172bb1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,15 +18,6 @@ "version" : "3.2.0" } }, - { - "identity" : "sourcekitten", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/SourceKitten", - "state" : { - "revision" : "eb6656ed26bdef967ad8d07c27e2eab34dc582f2", - "version" : "0.37.0" - } - }, { "identity" : "spectre", "kind" : "remoteSourceControl", @@ -37,21 +28,12 @@ } }, { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" - } - }, - { - "identity" : "swxmlhash", + "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/drmohundro/SWXMLHash.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", - "version" : "7.0.2" + "revision" : "4799286537280063c85a32f09884cfbca301b1a1", + "version" : "602.0.0" } }, { diff --git a/Package.swift b/Package.swift index fa71b61..ddb6794 100644 --- a/Package.swift +++ b/Package.swift @@ -10,17 +10,21 @@ let package = Package( .executable(name: "ribscodegen", targets: ["RIBsCodeGen"]) ], dependencies: [ - .package(url: "https://github.com/jpsim/SourceKitten", from: "0.37.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"), .package(url: "https://github.com/kylef/PathKit", from: "1.0.1"), .package(url: "https://github.com/onevcat/Rainbow", from: "3.2.0") ], targets: [ .executableTarget( - name: "RIBsCodeGen", + name: "RIBsCodeGen", dependencies: [ - .product(name: "SourceKittenFramework", package: "SourceKitten"), + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), "PathKit", - "Rainbow"] + "Yams", + "Rainbow" + ] ) ] ) diff --git a/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift b/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift index ce19357..980854f 100644 --- a/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift +++ b/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift @@ -6,7 +6,6 @@ // import Foundation -import SourceKittenFramework import PathKit struct CreateComponentExtension: Command { diff --git a/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift b/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift index bcb00c7..d9432f9 100644 --- a/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift +++ b/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift @@ -6,7 +6,6 @@ // import Foundation -import SourceKittenFramework import PathKit struct CreateRIBCommand: Command { diff --git a/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift b/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift index b665f05..d7882d5 100644 --- a/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift +++ b/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift @@ -3,9 +3,6 @@ // import Foundation - -import Foundation -import SourceKittenFramework import PathKit struct DeleteRIBCommand: Command { diff --git a/Sources/RIBsCodeGen/Commands/DependencyCommand.swift b/Sources/RIBsCodeGen/Commands/DependencyCommand.swift index 45e2add..eedf5ac 100644 --- a/Sources/RIBsCodeGen/Commands/DependencyCommand.swift +++ b/Sources/RIBsCodeGen/Commands/DependencyCommand.swift @@ -6,7 +6,6 @@ // import Foundation -import SourceKittenFramework import Rainbow import PathKit @@ -212,11 +211,10 @@ private extension DependencyCommand { var initArgumentEndPosition = 0 - guard let lastArgumentLength = initArguments.last?["key.length"] as? Int64, - let lastArgumentOffset = initArguments.last?["key.offset"] as? Int64 else { + guard let lastArgument = initArguments.last else { return } - initArgumentEndPosition = Int(lastArgumentOffset + lastArgumentLength) + initArgumentEndPosition = lastArgument.getKeyOffset() + lastArgument.getKeyLength() var text = try String.init(contentsOfFile: parentRouterFile.path!, encoding: .utf8) let argumentInsertIndex = text.utf8.index(text.startIndex, offsetBy: initArgumentEndPosition) diff --git a/Sources/RIBsCodeGen/Commands/RenameCommand.swift b/Sources/RIBsCodeGen/Commands/RenameCommand.swift index d3997f1..64c7554 100644 --- a/Sources/RIBsCodeGen/Commands/RenameCommand.swift +++ b/Sources/RIBsCodeGen/Commands/RenameCommand.swift @@ -3,7 +3,6 @@ // import Foundation -import SourceKittenFramework import Rainbow import PathKit diff --git a/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift b/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift index 5eca4fc..fad8f3e 100644 --- a/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift +++ b/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift @@ -3,7 +3,6 @@ // import Foundation -import SourceKittenFramework import PathKit struct UnlinkCommand: Command { diff --git a/Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift b/Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift index c798a04..6a7dd0d 100644 --- a/Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift +++ b/Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift @@ -6,78 +6,45 @@ // import Foundation -import SourceKittenFramework -extension Collection where Iterator.Element == [String: SourceKitRepresentable] { - func extractDictionaryContainsKeyName(_ targetKeyName: String) -> [String: SourceKitRepresentable] { - let targetStructures = self.filter { structure -> Bool in - let keyName = structure["key.name"] as? String ?? "" - return keyName.contains(targetKeyName) - } - return targetStructures.first ?? [String: SourceKitRepresentable]() +extension Collection where Iterator.Element == SwiftNode { + func extractDictionaryContainsKeyName(_ targetKeyName: String) -> SwiftNode { + first(where: { $0.name.contains(targetKeyName) }) ?? .empty } - func extractDictionaryContainsKeyNameLast(_ targetKeyName: String) -> [String: SourceKitRepresentable] { - let targetStructures = self.filter { structure -> Bool in - let keyName = structure["key.name"] as? String ?? "" - return keyName.contains(targetKeyName) - } - return targetStructures.last ?? [String: SourceKitRepresentable]() + func extractDictionaryContainsKeyNameLast(_ targetKeyName: String) -> SwiftNode { + reversed().first(where: { $0.name.contains(targetKeyName) }) ?? .empty } - func extractDictionaryByKeyName(_ targetKeyName: String) -> [String: SourceKitRepresentable] { - let targetStructures = self.filter { structure -> Bool in - let keyName = structure["key.name"] as? String ?? "" - return keyName == targetKeyName - } - return targetStructures.first ?? [String: SourceKitRepresentable]() + func extractDictionaryByKeyName(_ targetKeyName: String) -> SwiftNode { + first(where: { $0.name == targetKeyName }) ?? .empty } - func filterByKeyName(_ targetKeyName: String) -> [[String: SourceKitRepresentable]] { - let targetStructures = self.filter { structure -> Bool in - let keyName = structure["key.name"] as? String ?? "" - return keyName.contains(targetKeyName) - } - return targetStructures + func filterByKeyName(_ targetKeyName: String) -> [SwiftNode] { + filter { $0.name.contains(targetKeyName) } } - func filterByAttribute(_ targetKind: SwiftDeclarationAttributeKind) -> [[String: SourceKitRepresentable]] { - let targetStructures = self.filter { structure -> Bool in - let keyAttributes = structure["key.attribute"] as? String ?? "" - let attributeKind = SwiftDeclarationAttributeKind(rawValue: keyAttributes) + func filterByKeyKind(_ targetKind: SwiftDeclarationKind) -> [SwiftNode] { + filter { SwiftDeclarationKind(rawValue: $0.kind) == targetKind } + } - return attributeKind == targetKind - } - return targetStructures + func filterByKeyKind(_ targetKind: SwiftExpressionKind) -> [SwiftNode] { + filter { SwiftExpressionKind(rawValue: $0.kind) == targetKind } } - func filterByKeyKind(_ targetKind: SwiftDeclarationKind) -> [[String: SourceKitRepresentable]] { - let targetStructures = self.filter { initStructure -> Bool in - guard let kindValue = initStructure["key.kind"] as? String else { - return false - } - let kind = SwiftDeclarationKind(rawValue: kindValue) - return kind == targetKind - } - return targetStructures + func filterByKeyTypeName(_ targetTypeName: String) -> [SwiftNode] { + filter { $0.typeName.contains(targetTypeName) } } +} - func filterByKeyKind(_ targetKind: SwiftExpressionKind) -> [[String: SourceKitRepresentable]] { - let targetStructures = self.filter { initStructure -> Bool in - guard let kindValue = initStructure["key.kind"] as? String else { - return false - } - let kind = SwiftExpressionKind(rawValue: kindValue) - return kind == targetKind - } - return targetStructures +extension Collection where Iterator.Element == SwiftAttribute { + func filterByAttribute(_ targetKind: SwiftDeclarationAttributeKind) -> [SwiftAttribute] { + filter { $0.kind == targetKind } } +} - func filterByKeyTypeName(_ targetTypeName: String) -> [[String: SourceKitRepresentable]] { - let targetStructures = self.filter { structure -> Bool in - let keyTypeName = structure["key.typename"] as? String ?? "" - return keyTypeName.contains(targetTypeName) - } - return targetStructures +extension Collection where Iterator.Element == SwiftInheritedType { + func filterByKeyName(_ targetKeyName: String) -> [SwiftInheritedType] { + filter { $0.name.contains(targetKeyName) } } } diff --git a/Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift b/Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift index 9e07a22..ad23f1c 100644 --- a/Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift +++ b/Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift @@ -6,107 +6,79 @@ // import Foundation -import SourceKittenFramework -extension Dictionary where Key == String { +extension SwiftNode { func getKeyName() -> String { - self["key.name"] as? String ?? "" + name } - + func getTypeName() -> String { - self["key.typename"] as? String ?? "" + typeName } func getDeclarationKind() -> SwiftDeclarationKind? { - guard let kindValue = self["key.kind"] as? String else { - return nil - } - return SwiftDeclarationKind(rawValue: kindValue) - } - - func getAttributes() -> [[String: SourceKitRepresentable]] { - guard let attributes = self["key.attributes"] as? [[String: SourceKitRepresentable]] else { - return [[String: SourceKitRepresentable]]() - } - return attributes + SwiftDeclarationKind(rawValue: kind) } - func getInheritedTypes() -> [[String: SourceKitRepresentable]] { - guard let inheritedtypes = self["key.inheritedtypes"] as? [[String: SourceKitRepresentable]] else { - return [[String: SourceKitRepresentable]]() - } - return inheritedtypes + func getAttributes() -> [SwiftAttribute] { + attributes } - func getElements() -> [[String: SourceKitRepresentable]] { - guard let elements = self["key.elements"] as? [[String: SourceKitRepresentable]] else { - return [[String: SourceKitRepresentable]]() - } - return elements + func getInheritedTypes() -> [SwiftInheritedType] { + inheritedTypes } - func getSubStructures() -> [[String: SourceKitRepresentable]] { - guard let substructures = self["key.substructure"] as? [[String: SourceKitRepresentable]] else { - return [[String: SourceKitRepresentable]]() - } - return substructures + func getSubStructures() -> [SwiftNode] { + substructures } func getOuterLeadingPosition() -> Int { // 外側の先頭の位置を確認する 【ここ→self.functionName()】 - let targetLeadingPosition = self["key.nameoffset"] as? Int64 ?? 0 - return Int(targetLeadingPosition) + nameOffset } func getInnerLeadingPosition() -> Int { // 内側の先頭の位置を確認する 【self.functionName(←ここ)】 - let targetLeadingPosition = self["key.bodyoffset"] as? Int64 ?? 0 - return Int(targetLeadingPosition) + bodyOffset } func getInnerTrailingPosition() -> Int { // 内側の末尾の位置を確認する 【self.functionName(ここ→)】 - let targetBodyOffset = self["key.bodyoffset"] as? Int64 ?? 0 - let targetBodyLength = self["key.bodylength"] as? Int64 ?? 0 - return Int(targetBodyOffset + targetBodyLength) + bodyOffset + bodyLength } func getOuterTrailingPosition() -> Int { // 外側の末尾の位置を確認する 【self.functionName()←ここ】 return getInnerTrailingPosition() + 1 } - + func getVariableTypeLeadingPosition() -> Int { // プロパティの型の先頭の位置を確認する【var router: ここ→OrderRouting?】 - let targetNameOffset = self["key.nameoffset"] as? Int64 ?? 0 - let targetNameLength = self["key.namelength"] as? Int64 ?? 0 - return Int(targetNameOffset + targetNameLength) + nameOffset + nameLength } - + func getVariableTypeTrailingPosition() -> Int { // プロパティの型の先頭の位置を確認する【var router: OrderRouting?←ここ】 - let targetOffset = self["key.offset"] as? Int64 ?? 0 - let targetLength = self["key.length"] as? Int64 ?? 0 - return Int(targetOffset + targetLength) + offset + length } - + func getKeyNameLength() -> Int { - Int(self["key.namelength"] as? Int64 ?? 0) + nameLength } - + func getKeyLength() -> Int { - Int(self["key.length"] as? Int64 ?? 0) + length } - + func getKeyOffset() -> Int { - Int(self["key.offset"] as? Int64 ?? 0) + offset } - + func getKeyBodyOffset() -> Int { - Int(self["key.bodyoffset"] as? Int64 ?? 0) + bodyOffset } - + func getKeyBodyLength() -> Int { - Int(self["key.bodylength"] as? Int64 ?? 0) + bodyLength } } diff --git a/Sources/RIBsCodeGen/Models/Formatter.swift b/Sources/RIBsCodeGen/Models/Formatter.swift index 354eff1..693ceb1 100644 --- a/Sources/RIBsCodeGen/Models/Formatter.swift +++ b/Sources/RIBsCodeGen/Models/Formatter.swift @@ -6,7 +6,6 @@ // import Foundation -import SourceKittenFramework private enum FormatterError: Swift.Error { case notFoundTargetFile diff --git a/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift b/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift new file mode 100644 index 0000000..0aeeb57 --- /dev/null +++ b/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift @@ -0,0 +1,712 @@ +import Foundation +import SwiftParser +import SwiftSyntax + +enum SwiftDeclarationKind: String, CaseIterable { + case `protocol` = "source.lang.swift.decl.protocol" + case `class` = "source.lang.swift.decl.class" + case `struct` = "source.lang.swift.decl.struct" + case `extension` = "source.lang.swift.decl.extension" + case functionConstructor = "source.lang.swift.decl.function.constructor" + case functionMethodInstance = "source.lang.swift.decl.function.method.instance" + case varInstance = "source.lang.swift.decl.var.instance" + case varParameter = "source.lang.swift.decl.var.parameter" +} + +enum SwiftDeclarationAttributeKind: String, CaseIterable { + case `override` = "source.decl.attribute.override" +} + +struct SwiftAttribute { + let kind: SwiftDeclarationAttributeKind +} + +struct SwiftInheritedType { + let name: String +} + +struct ByteCount { + let value: Int +} + +struct ByteRange { + let location: ByteCount + let length: ByteCount +} + +struct Line { + let index: Int + let content: String + let byteRange: ByteRange +} + +final class File { + let path: String? + private(set) var contents: String + + var lines: [Line] { + LineParser.parse(contents: contents) + } + + init?(path: String) { + self.path = URL(fileURLWithPath: path).path + guard let loaded = try? String(contentsOfFile: path, encoding: .utf8) else { + return nil + } + contents = loaded + } + + init(contents: String) { + path = nil + self.contents = contents + } + + func format(trimmingTrailingWhitespace: Bool, + useTabs: Bool, + indentWidth: Int) throws -> String { + SwiftFileFormatter.format( + contents: contents, + trimmingTrailingWhitespace: trimmingTrailingWhitespace, + useTabs: useTabs, + indentWidth: indentWidth + ) + } +} + +struct Structure { + let dictionary: SwiftNode + + init(file: File) throws { + dictionary = SwiftSyntaxStructureParser.makeRoot(source: file.contents) + } +} + +struct SwiftNode { + let name: String + let typeName: String + let kind: String + let offset: Int + let length: Int + let nameOffset: Int + let nameLength: Int + let bodyOffset: Int + let bodyLength: Int + let attributes: [SwiftAttribute] + let inheritedTypes: [SwiftInheritedType] + let substructures: [SwiftNode] + let isEmpty: Bool + + static let empty = SwiftNode( + name: "", + typeName: "", + kind: "", + offset: 0, + length: 0, + nameOffset: 0, + nameLength: 0, + bodyOffset: 0, + bodyLength: 0, + attributes: [], + inheritedTypes: [], + substructures: [], + isEmpty: true + ) +} + +private enum SwiftSyntaxExpressionKind: String { + case call = "source.lang.swift.expr.call" + case argument = "source.lang.swift.expr.argument" +} + +private enum LineParser { + static func parse(contents: String) -> [Line] { + let splitLines = contents.split(separator: "\n", omittingEmptySubsequences: false) + var offset = 0 + var result: [Line] = [] + + for (index, element) in splitLines.enumerated() { + let line = String(element) + let hasTerminator = index < splitLines.count - 1 + let length = line.lengthOfBytes(using: .utf8) + (hasTerminator ? 1 : 0) + result.append( + Line( + index: index + 1, + content: line, + byteRange: ByteRange(location: ByteCount(value: offset), length: ByteCount(value: length)) + ) + ) + offset += length + } + + return result + } +} + +private enum SwiftFileFormatter { + static func format(contents: String, + trimmingTrailingWhitespace: Bool, + useTabs: Bool, + indentWidth: Int) -> String { + let rawLines = contents.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) + var formattedLines: [String] = [] + var braceDepth = 0 + var parenStack: [Int] = [] + var inheritanceIndent: Int? + + for rawLine in rawLines { + let trimmedLine = rawLine.trimmingCharacters(in: .whitespaces) + + guard !trimmedLine.isEmpty else { + formattedLines.append("") + continue + } + + let leadingClosingBraces = trimmedLine.leadingClosingBraceCount + let baseBraceDepth = max(0, braceDepth - leadingClosingBraces) + let baseIndent = baseBraceDepth * indentWidth + + let indent: Int + if let inheritanceIndent, !trimmedLine.startsTypeDeclaration { + indent = inheritanceIndent + } else if let continuationIndent = parenStack.last, !trimmedLine.hasPrefix("}") { + indent = continuationIndent + } else { + indent = baseIndent + } + + let normalizedLine = indentationString(width: indent, useTabs: useTabs, indentWidth: indentWidth) + trimmedLine + formattedLines.append(normalizedLine) + + updateContext( + line: normalizedLine, + braceDepth: &braceDepth, + parenStack: &parenStack, + inheritanceIndent: &inheritanceIndent + ) + } + + var output = formattedLines.joined(separator: "\n") + if trimmingTrailingWhitespace { + output = output + .split(separator: "\n", omittingEmptySubsequences: false) + .map { $0.removingTrailingWhitespace } + .joined(separator: "\n") + } + + if !output.isEmpty && !output.hasSuffix("\n") { + output.append("\n") + } + + return output + } + + private static func indentationString(width: Int, useTabs: Bool, indentWidth: Int) -> String { + guard useTabs else { + return String(repeating: " ", count: width) + } + + let tabCount = width / max(1, indentWidth) + let spaceCount = width % max(1, indentWidth) + return String(repeating: "\t", count: tabCount) + String(repeating: " ", count: spaceCount) + } + + private static func updateContext(line: String, + braceDepth: inout Int, + parenStack: inout [Int], + inheritanceIndent: inout Int?) { + inheritanceIndent = nextInheritanceIndent(current: inheritanceIndent, line: line) + + var isInsideString = false + var isEscaped = false + let characters = Array(line) + var index = 0 + + while index < characters.count { + let character = characters[index] + + if isInsideString { + if isEscaped { + isEscaped = false + } else if character == "\\" { + isEscaped = true + } else if character == "\"" { + isInsideString = false + } + index += 1 + continue + } + + if character == "/" && index + 1 < characters.count && characters[index + 1] == "/" { + break + } + + switch character { + case "\"": + isInsideString = true + case "(": + parenStack.append(index + 1) + case ")": + if !parenStack.isEmpty { + parenStack.removeLast() + } + case "{": + braceDepth += 1 + case "}": + braceDepth = max(0, braceDepth - 1) + default: + break + } + + index += 1 + } + } + + private static func nextInheritanceIndent(current: Int?, line: String) -> Int? { + let trimmedLine = line.trimmingCharacters(in: .whitespaces) + + if trimmedLine.startsTypeDeclaration, + let colonIndex = line.firstIndex(of: ":"), + (!trimmedLine.contains("{") || trimmedLine.hasSuffix(",")) { + let firstInheritedColumn = line.distance(from: line.startIndex, to: colonIndex) + 2 + return firstInheritedColumn + } + + if current != nil && trimmedLine.contains("{") { + return nil + } + + return current + } +} + +private enum SwiftSyntaxStructureParser { + static func makeRoot(source: String) -> SwiftNode { + let sourceFile = Parser.parse(source: source) + var substructures: [SwiftNode] = [] + + for statement in sourceFile.statements { + if let decl = statement.item.as(DeclSyntax.self) { + substructures.append(contentsOf: parseDecl(decl, source: source)) + } + } + + return SwiftNode( + name: "", + typeName: "", + kind: "source.lang.swift.decl.root", + offset: 0, + length: source.lengthOfBytes(using: .utf8), + nameOffset: 0, + nameLength: 0, + bodyOffset: 0, + bodyLength: source.lengthOfBytes(using: .utf8), + attributes: [], + inheritedTypes: [], + substructures: substructures, + isEmpty: false + ) + } + + private static func parseDecl(_ decl: DeclSyntax, source: String) -> [SwiftNode] { + if let protocolDecl = decl.as(ProtocolDeclSyntax.self) { + return [parseProtocol(protocolDecl, source: source)] + } + if let classDecl = decl.as(ClassDeclSyntax.self) { + return [parseClass(classDecl, source: source)] + } + if let structDecl = decl.as(StructDeclSyntax.self) { + return [parseStruct(structDecl, source: source)] + } + if let extensionDecl = decl.as(ExtensionDeclSyntax.self) { + return [parseExtension(extensionDecl, source: source)] + } + if let variableDecl = decl.as(VariableDeclSyntax.self) { + return parseVar(variableDecl, as: .varInstance) + } + if let initializerDecl = decl.as(InitializerDeclSyntax.self) { + return [parseInitializer(initializerDecl, source: source)] + } + if let functionDecl = decl.as(FunctionDeclSyntax.self) { + return [parseFunction(functionDecl, source: source)] + } + return [] + } + + private static func parseProtocol(_ node: ProtocolDeclSyntax, source: String) -> SwiftNode { + let children = node.memberBlock.members.flatMap { parseDecl($0.decl, source: source) } + return makeDeclNode( + name: node.name.text, + kind: SwiftDeclarationKind.protocol.rawValue, + syntax: Syntax(node), + nameSyntax: Syntax(node.name), + bodySyntax: Syntax(node.memberBlock), + attributes: parseAttributes(node.attributes), + inheritedTypes: parseInheritedTypes(node.inheritanceClause), + children: children + ) + } + + private static func parseClass(_ node: ClassDeclSyntax, source: String) -> SwiftNode { + let children = node.memberBlock.members.flatMap { parseDecl($0.decl, source: source) } + return makeDeclNode( + name: node.name.text, + kind: SwiftDeclarationKind.class.rawValue, + syntax: Syntax(node), + nameSyntax: Syntax(node.name), + bodySyntax: Syntax(node.memberBlock), + attributes: parseAttributes(node.attributes), + inheritedTypes: parseInheritedTypes(node.inheritanceClause), + children: children + ) + } + + private static func parseStruct(_ node: StructDeclSyntax, source: String) -> SwiftNode { + let children = node.memberBlock.members.flatMap { parseDecl($0.decl, source: source) } + return makeDeclNode( + name: node.name.text, + kind: SwiftDeclarationKind.struct.rawValue, + syntax: Syntax(node), + nameSyntax: Syntax(node.name), + bodySyntax: Syntax(node.memberBlock), + attributes: parseAttributes(node.attributes), + inheritedTypes: parseInheritedTypes(node.inheritanceClause), + children: children + ) + } + + private static func parseExtension(_ node: ExtensionDeclSyntax, source: String) -> SwiftNode { + let children = node.memberBlock.members.flatMap { parseDecl($0.decl, source: source) } + return makeDeclNode( + name: node.extendedType.trimmedDescription, + kind: SwiftDeclarationKind.extension.rawValue, + syntax: Syntax(node), + nameSyntax: Syntax(node.extendedType), + bodySyntax: Syntax(node.memberBlock), + attributes: parseAttributes(node.attributes), + inheritedTypes: parseInheritedTypes(node.inheritanceClause), + children: children + ) + } + + private static func parseVar(_ node: VariableDeclSyntax, as kind: SwiftDeclarationKind) -> [SwiftNode] { + node.bindings.compactMap { binding in + guard let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self) else { + return nil + } + + return makeDeclNode( + name: identifierPattern.identifier.text, + kind: kind.rawValue, + syntax: Syntax(binding), + nameSyntax: Syntax(identifierPattern.identifier), + bodySyntax: nil, + typeName: binding.typeAnnotation?.type.trimmedDescription ?? "", + attributes: [], + inheritedTypes: [], + children: [] + ) + } + } + + private static func parseInitializer(_ node: InitializerDeclSyntax, source: String) -> SwiftNode { + let parameterNodes = parseParameters(node.signature.parameterClause.parameters) + let callNodes = parseCallNodes(node.body, source: source) + let children = parameterNodes + callNodes + let labels = node.signature.parameterClause.parameters.map { parameterLabel(for: $0) }.joined() + + return makeDeclNode( + name: "init(\(labels))", + kind: SwiftDeclarationKind.functionConstructor.rawValue, + syntax: Syntax(node), + nameSyntax: Syntax(node.initKeyword), + bodySyntax: node.body.map { Syntax($0) }, + attributes: parseDeclarationAttributes(attributes: node.attributes, modifiers: node.modifiers), + inheritedTypes: [], + children: children + ) + } + + private static func parseFunction(_ node: FunctionDeclSyntax, source: String) -> SwiftNode { + let parameterNodes = parseParameters(node.signature.parameterClause.parameters) + let callNodes = parseCallNodes(node.body, source: source) + let children = parameterNodes + callNodes + let labels = node.signature.parameterClause.parameters.map { parameterLabel(for: $0) }.joined() + + return makeDeclNode( + name: "\(node.name.text)(\(labels))", + kind: SwiftDeclarationKind.functionMethodInstance.rawValue, + syntax: Syntax(node), + nameSyntax: Syntax(node.name), + bodySyntax: node.body.map { Syntax($0) }, + typeName: node.signature.returnClause?.type.trimmedDescription ?? "", + attributes: parseDeclarationAttributes(attributes: node.attributes, modifiers: node.modifiers), + inheritedTypes: [], + children: children + ) + } + + private static func parameterLabel(for parameter: FunctionParameterSyntax) -> String { + let label = parameter.firstName.text + if label == "_" { + return "_:" + } + return "\(label):" + } + + private static func parseParameters(_ parameters: FunctionParameterListSyntax) -> [SwiftNode] { + parameters.map { parameter in + let parameterNameToken = parameter.secondName ?? parameter.firstName + let parameterName: String + if let secondName = parameter.secondName, secondName.text != "_" { + parameterName = secondName.text + } else { + parameterName = parameter.firstName.text + } + + return makeDeclNode( + name: parameterName, + kind: SwiftDeclarationKind.varParameter.rawValue, + syntax: Syntax(parameter), + nameSyntax: Syntax(parameterNameToken), + bodySyntax: nil, + typeName: parameter.type.trimmedDescription, + attributes: [], + inheritedTypes: [], + children: [] + ) + } + } + + private static func parseCallNodes(_ body: CodeBlockSyntax?, source: String) -> [SwiftNode] { + guard let body else { + return [] + } + let collector = CallCollector(source: source) + collector.walk(Syntax(body)) + return collector.collectedCalls + } + + private static func parseAttributes(_ attributes: AttributeListSyntax?) -> [SwiftAttribute] { + guard let attributes else { + return [] + } + + var result: [SwiftAttribute] = [] + for attributeElement in attributes { + guard let attribute = attributeElement.as(AttributeSyntax.self) else { + continue + } + if attribute.attributeName.trimmedDescription == "override" { + result.append(SwiftAttribute(kind: .override)) + } + } + return result + } + + private static func parseDeclarationAttributes(attributes: AttributeListSyntax?, + modifiers: DeclModifierListSyntax?) -> [SwiftAttribute] { + var result = parseAttributes(attributes) + result.append(contentsOf: parseModifiers(modifiers)) + return result + } + + private static func parseModifiers(_ modifiers: DeclModifierListSyntax?) -> [SwiftAttribute] { + guard let modifiers else { + return [] + } + return modifiers.compactMap { modifier in + modifier.name.text == "override" ? SwiftAttribute(kind: .override) : nil + } + } + + private static func parseInheritedTypes(_ inheritance: InheritanceClauseSyntax?) -> [SwiftInheritedType] { + guard let inheritance else { + return [] + } + return inheritance.inheritedTypes.map { inheritedType in + SwiftInheritedType(name: inheritedType.type.trimmedDescription) + } + } + + private static func makeDeclNode(name: String, + kind: String, + syntax: Syntax, + nameSyntax: Syntax?, + bodySyntax: Syntax?, + typeName: String = "", + attributes: [SwiftAttribute], + inheritedTypes: [SwiftInheritedType], + children: [SwiftNode]) -> SwiftNode { + let offset = startOffset(of: syntax) + let end = endOffset(of: syntax) + let nameOffset = nameSyntax.map(startOffset(of:)) ?? offset + let nameLength = name.lengthOfBytes(using: .utf8) + let bodyRange = bodySyntax.flatMap(extractBodyRange(of:)) ?? (offset: 0, length: 0) + + return SwiftNode( + name: name, + typeName: typeName, + kind: kind, + offset: offset, + length: max(0, end - offset), + nameOffset: nameOffset, + nameLength: nameLength, + bodyOffset: bodyRange.offset, + bodyLength: bodyRange.length, + attributes: attributes, + inheritedTypes: inheritedTypes, + substructures: children, + isEmpty: false + ) + } + + private static func startOffset(of syntax: Syntax) -> Int { + syntax.positionAfterSkippingLeadingTrivia.utf8Offset + } + + private static func endOffset(of syntax: Syntax) -> Int { + syntax.endPositionBeforeTrailingTrivia.utf8Offset + } + + private static func extractBodyRange(of syntax: Syntax) -> (offset: Int, length: Int)? { + if let memberBlock = syntax.as(MemberBlockSyntax.self) { + let offset = memberBlock.leftBrace.endPositionBeforeTrailingTrivia.utf8Offset + let end = memberBlock.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset + return (offset, max(0, end - offset)) + } + + if let codeBlock = syntax.as(CodeBlockSyntax.self) { + let offset = codeBlock.leftBrace.endPositionBeforeTrailingTrivia.utf8Offset + let end = codeBlock.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset + return (offset, max(0, end - offset)) + } + + return nil + } +} + +private final class CallCollector: SyntaxVisitor { + private let source: String + fileprivate var collectedCalls: [SwiftNode] = [] + + init(source: String) { + self.source = source + super.init(viewMode: .sourceAccurate) + } + + override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { + collectedCalls.append(parseCall(node)) + return .visitChildren + } + + private func parseCall(_ node: FunctionCallExprSyntax) -> SwiftNode { + let offset = node.positionAfterSkippingLeadingTrivia.utf8Offset + let end = node.endPositionBeforeTrailingTrivia.utf8Offset + let calledExpression = node.calledExpression.trimmedDescription + let arguments: [SwiftNode] = node.arguments.map { argument in + let argumentOffset = argument.positionAfterSkippingLeadingTrivia.utf8Offset + let argumentEnd = argument.endPositionBeforeTrailingTrivia.utf8Offset + let argumentName = argument.label?.text ?? "" + return SwiftNode( + name: argumentName, + typeName: argument.expression.trimmedDescription, + kind: SwiftSyntaxExpressionKind.argument.rawValue, + offset: argumentOffset, + length: max(0, argumentEnd - argumentOffset), + nameOffset: argumentOffset, + nameLength: argumentName.lengthOfBytes(using: .utf8), + bodyOffset: 0, + bodyLength: 0, + attributes: [], + inheritedTypes: [], + substructures: [], + isEmpty: false + ) + } + + var bodyOffset = offset + var bodyLength = 0 + if let range = callBodyRange(offset: offset, length: max(0, end - offset)) { + bodyOffset = range.location + bodyLength = range.length + } + + return SwiftNode( + name: calledExpression, + typeName: "", + kind: SwiftSyntaxExpressionKind.call.rawValue, + offset: offset, + length: max(0, end - offset), + nameOffset: offset, + nameLength: calledExpression.lengthOfBytes(using: .utf8), + bodyOffset: bodyOffset, + bodyLength: bodyLength, + attributes: [], + inheritedTypes: [], + substructures: arguments, + isEmpty: false + ) + } + + private func callBodyRange(offset: Int, length: Int) -> (location: Int, length: Int)? { + guard length > 0 else { + return nil + } + guard let callText = source.utf8Substring(start: offset, length: length) else { + return nil + } + guard let openParen = callText.firstIndex(of: "("), + let closeParen = callText.lastIndex(of: ")") else { + return nil + } + let openOffset = callText.distance(from: callText.startIndex, to: openParen) + let closeOffset = callText.distance(from: callText.startIndex, to: closeParen) + let bodyStart = offset + openOffset + 1 + let bodyLength = max(0, closeOffset - openOffset - 1) + return (location: bodyStart, length: bodyLength) + } +} + +private extension StringProtocol { + var removingTrailingWhitespace: String { + var result = String(self) + while let last = result.last, last == " " || last == "\t" { + result.removeLast() + } + return result + } +} + +private extension String { + var startsTypeDeclaration: Bool { + let trimmed = trimmingCharacters(in: .whitespaces) + return trimmed.hasPrefix("protocol ") + || trimmed.hasPrefix("class ") + || trimmed.hasPrefix("struct ") + || trimmed.hasPrefix("extension ") + } + + var leadingClosingBraceCount: Int { + var count = 0 + for character in trimmingCharacters(in: .whitespaces) { + if character == "}" { + count += 1 + } else { + break + } + } + return count + } + + func utf8Substring(start: Int, length: Int) -> String? { + guard start >= 0, length >= 0 else { + return nil + } + guard start <= utf8.count, start + length <= utf8.count else { + return nil + } + let startIndex = utf8.index(utf8.startIndex, offsetBy: start) + let endIndex = utf8.index(startIndex, offsetBy: length) + return String(decoding: utf8[startIndex.. Date: Fri, 20 Mar 2026 03:49:58 +0900 Subject: [PATCH 2/6] Format source files --- .../Commands/CreateComponentExtension.swift | 1 - .../Commands/CreateRIBCommand.swift | 1 - .../Commands/DeleteRIBCommand.swift | 10 +- .../Commands/DependencyCommand.swift | 4 +- .../RIBsCodeGen/Commands/HelpCommand.swift | 4 +- .../RIBsCodeGen/Commands/RenameCommand.swift | 116 +++++++++--------- .../RIBsCodeGen/Commands/UnlinkCommand.swift | 50 ++++---- Sources/RIBsCodeGen/Models/Argument.swift | 2 +- Sources/RIBsCodeGen/Models/Formatter.swift | 1 - Sources/RIBsCodeGen/Models/Shell.swift | 6 +- .../Models/SwiftSyntaxSupport.swift | 2 +- Sources/RIBsCodeGen/main.swift | 12 +- 12 files changed, 103 insertions(+), 106 deletions(-) diff --git a/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift b/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift index 980854f..21259b2 100644 --- a/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift +++ b/Sources/RIBsCodeGen/Commands/CreateComponentExtension.swift @@ -81,4 +81,3 @@ private extension CreateComponentExtension { try Path(filePath).write(replacedText) } } - diff --git a/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift b/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift index d9432f9..5d0abe4 100644 --- a/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift +++ b/Sources/RIBsCodeGen/Commands/CreateRIBCommand.swift @@ -43,7 +43,6 @@ struct CreateRIBCommand: Command { let parentDirectory = isNeedle ? setting.templateDirectory + "/Needle" : setting.templateDirectory + "/Normal" templateDirectory = isOwnsView ? parentDirectory + "/OwnsView" : parentDirectory + "/Default" - self.target = target self.isOwnsView = isOwnsView self.isNeedle = isNeedle diff --git a/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift b/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift index d7882d5..19bc00d 100644 --- a/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift +++ b/Sources/RIBsCodeGen/Commands/DeleteRIBCommand.swift @@ -8,15 +8,15 @@ import PathKit struct DeleteRIBCommand: Command { private let paths: [String] private let targetName: String - + init(paths: [String], targetName: String) { self.paths = paths self.targetName = targetName } - + func run() -> Result { print("\nStart deleting \(targetName) RIB.\n".bold) - + guard let interactorPath = paths.filter({ $0.contains("/\(targetName)/\(targetName)Interactor.swift") }).first else { return .failure(error: .failedToDelete("Not found \(targetName)/\(targetName)Interactor.swift. Interrupt deleting operation.")) } @@ -25,11 +25,11 @@ struct DeleteRIBCommand: Command { guard directoryPath.isDirectory else { return .failure(error: .failedToDelete("\(directoryPath) is not Directory. Failed to detect the target directory. Interrupt deleting operation.")) } - + print("\tDelete ".magenta + " \(directoryPath) ".onMagenta + ".".magenta) let shellResult = shell("rm -rf \(directoryPath)") print("\t\(shellResult)") - + return .success(message: "\nSuccessfully finished deleting \(targetName) RIB.".green.bold) } } diff --git a/Sources/RIBsCodeGen/Commands/DependencyCommand.swift b/Sources/RIBsCodeGen/Commands/DependencyCommand.swift index eedf5ac..30b30aa 100644 --- a/Sources/RIBsCodeGen/Commands/DependencyCommand.swift +++ b/Sources/RIBsCodeGen/Commands/DependencyCommand.swift @@ -23,7 +23,7 @@ struct DependencyCommand: Command { init(paths: [String], parent: String, child: String) { self.parent = parent self.child = child - + guard let parentInteractorPath = paths.filter({ $0.contains("/" + parent + "Interactor.swift") }).first else { fatalError("Not found \(parent)Interactor.swift".red.bold) } @@ -55,7 +55,7 @@ struct DependencyCommand: Command { self.childRouterPath = childRouterPath self.childBuilderPath = childBuilderPath } - + func run() -> Result { print("\nStart adding dependency and builder initialize".bold) diff --git a/Sources/RIBsCodeGen/Commands/HelpCommand.swift b/Sources/RIBsCodeGen/Commands/HelpCommand.swift index dc3b756..76f2f5d 100644 --- a/Sources/RIBsCodeGen/Commands/HelpCommand.swift +++ b/Sources/RIBsCodeGen/Commands/HelpCommand.swift @@ -32,10 +32,10 @@ struct HelpCommand: Command { - unlink RIB ribscodegen unlink [target RIB name] --parent [parent RIB name] - + - remove RIB ribscodegen remove [target RIB name] - + cf. More details in https://github.com/imairi/RIBsCodeGen/ """.lightBlue diff --git a/Sources/RIBsCodeGen/Commands/RenameCommand.swift b/Sources/RIBsCodeGen/Commands/RenameCommand.swift index 64c7554..c725d63 100644 --- a/Sources/RIBsCodeGen/Commands/RenameCommand.swift +++ b/Sources/RIBsCodeGen/Commands/RenameCommand.swift @@ -8,7 +8,7 @@ import PathKit private enum RenameProtocolType: CaseIterable { case routing, listener, presentable - + var suffix: String { switch self { case .routing: @@ -23,7 +23,7 @@ private enum RenameProtocolType: CaseIterable { private enum RenameVariableType: CaseIterable { case routing, listener, presentableListener - + var suffix: String { switch self { case .routing: @@ -45,7 +45,7 @@ final class RenameCommand: Command { private let renameSetting: RenameSetting private let currentName: String private let newName: String - + private let interactorPath: String private let routerPath: String private let builderPath: String @@ -60,37 +60,37 @@ final class RenameCommand: Command { self.renameSetting = renameSetting self.currentName = currentName self.newName = newName - + guard !paths.filter({ $0.contains("/" + currentName + "/") }).isEmpty else { fatalError("Not found \(currentName) RIB directory.Might be wrong the target name.".red.bold) } - + guard let interactorPath = paths.filter({ $0.contains("/" + currentName + "Interactor.swift") }).first else { fatalError("Not found \(currentName)Interactor.swift in \(currentName) RIB directory.".red.bold) } - + guard let routerPath = paths.filter({ $0.contains("/" + currentName + "Router.swift") }).first else { fatalError("Not found \(currentName)Router.swift in \(currentName) RIB directory.".red.bold) } - + guard let builderPath = paths.filter({ $0.contains("/" + currentName + "Builder.swift") }).first else { fatalError("Not found \(currentName)Builder.swift in \(currentName) RIB directory.".red.bold) } - + self.interactorPath = interactorPath self.routerPath = routerPath self.builderPath = builderPath viewControllerPath = paths.filter({ $0.contains("/" + currentName + "ViewController.swift") }).first currentDependenciesPath = paths.filter({ $0.contains("/" + currentName + "/Dependencies/") }) } - + func run() -> Result { getParents() print("\nStart renaming ".bold + currentName.applyingBackgroundColor(.magenta).bold + " to ".bold + newName.applyingBackgroundColor(.blue).bold + ".".bold) - + var result: Result? - + print("\n\tStart renaming codes for related files.".bold) let builderIsNeedle = validateBuilderIsNeedle(builderFilePath: builderPath) @@ -108,43 +108,43 @@ final class RenameCommand: Command { } catch { result = .failure(error: .failedToRename("Failed to rename operation for target Router.")) } - + do { try renameForBuilder() } catch { result = .failure(error: .failedToRename("Failed to rename operation for target Builder.")) } - + do { try renameForViewController() } catch { result = .failure(error: .failedToRename("Failed to rename operation for target ViewController.")) } - + do { try renameForDependencies() } catch { result = .failure(error: .failedToRename("Failed to rename operation for target Dependencies.")) } - + do { try renameForParentsInteractor() } catch { result = .failure(error: .failedToRename("Failed to rename operation for target parent Interactor.")) } - + do { try renameForParentsRouter() } catch { result = .failure(error: .failedToRename("Failed to rename operation for target parent Router.")) } - + do { try renameForParentsBuilder() } catch { result = .failure(error: .failedToRename("Failed to rename operation for target parent Builder.")) } - + if builderIsNeedle { do { try renameForParentsComponentExtensions() @@ -158,13 +158,13 @@ final class RenameCommand: Command { } catch { result = .failure(error: .failedFormat) } - + do { try renameDirectoriesAndFiles() } catch { result = .failure(error: .failedFormat) } - + return result ?? .success(message: "\nSuccessfully finished renaming ".green.bold + currentName.applyingBackgroundColor(.magenta).green.bold + " to ".green.bold + newName.applyingBackgroundColor(.blue).green.bold + " for related files.".green.bold) } } @@ -204,7 +204,7 @@ private extension RenameCommand { let parentBuilderClasses = parentBuilderFileStructure.dictionary.getSubStructures().filterByKeyKind(.class) if let parentComponentClass = parentBuilderClasses.filterByKeyName("\(parent)Component").first, - let _ = parentComponentClass.getSubStructures().filterByKeyTypeName("\(currentName)Component").first { + let _ = parentComponentClass.getSubStructures().filterByKeyTypeName("\(currentName)Component").first { return true } else { return false @@ -219,11 +219,11 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: interactorSearchText, with: newName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(interactorPath).write(replacedText) replacedFilePaths.append(interactorPath) } - + func renameForRouter() throws { print("\t\trename for \(routerPath.lastElementSplittedBySlash)") let text = try String.init(contentsOfFile: routerPath, encoding: .utf8) @@ -235,7 +235,7 @@ private extension RenameCommand { try Path(routerPath).write(replacedText) replacedFilePaths.append(routerPath) } - + func renameForBuilder() throws { print("\t\trename for \(builderPath.lastElementSplittedBySlash)") let text = try String.init(contentsOfFile: builderPath, encoding: .utf8) @@ -244,11 +244,11 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: builderSearchText, with: newName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(builderPath).write(replacedText) replacedFilePaths.append(builderPath) } - + func renameForViewController() throws { guard let viewControllerPath = viewControllerPath else { return @@ -260,11 +260,11 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: viewControllerSearchText, with: newName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(viewControllerPath).write(replacedText) replacedFilePaths.append(viewControllerPath) } - + func renameForDependencies() throws { try currentDependenciesPath.forEach { dependencyPath in print("\t\trename for \(dependencyPath.lastElementSplittedBySlash)") @@ -274,12 +274,12 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: componentExtensionSearchText, with: newName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(dependencyPath).write(replacedText) replacedFilePaths.append(dependencyPath) } } - + func renameForParentsInteractor() throws { try parents.forEach { parentName in guard let parentInteractorPath = paths.filter({ $0.contains("/" + parentName + "Interactor.swift") }).first else { @@ -287,7 +287,7 @@ private extension RenameCommand { print("Skip to rename codes in \(parentName)Interactor.swift.".yellow.bold) return } - + print("\t\trename for \(parentInteractorPath.lastElementSplittedBySlash)") let text = try String.init(contentsOfFile: parentInteractorPath, encoding: .utf8) let replacedText = renameSetting.parentInteractor.reduce(text) { (result, parentInteractorSearchText) in @@ -295,12 +295,12 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: parentInteractorSearchText, with: newName, and: parentName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(parentInteractorPath).write(replacedText) replacedFilePaths.append(parentInteractorPath) } } - + func renameForParentsRouter() throws { try parents.forEach { parentName in guard let parentRouterPath = paths.filter({ $0.contains("/" + parentName + "Router.swift") }).first else { @@ -308,7 +308,7 @@ private extension RenameCommand { print("Skip to rename codes in \(parentName)Router.swift.".yellow.bold) return } - + print("\t\trename for \(parentRouterPath.lastElementSplittedBySlash)") let text = try String.init(contentsOfFile: parentRouterPath, encoding: .utf8) let replacedText = renameSetting.parentRouter.reduce(text) { (result, parentRouterSearchText) in @@ -316,12 +316,12 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: parentRouterSearchText, with: newName, and: parentName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(parentRouterPath).write(replacedText) replacedFilePaths.append(parentRouterPath) } } - + func renameForParentsBuilder() throws { try parents.forEach { parentName in guard let parentBuilderPath = paths.filter({ $0.contains("/" + parentName + "Builder.swift") }).first else { @@ -329,7 +329,7 @@ private extension RenameCommand { print("Skip to rename codes in \(parentName)Builder.swift.".yellow.bold) return } - + print("\t\trename for \(parentBuilderPath.lastElementSplittedBySlash)") let text = try String.init(contentsOfFile: parentBuilderPath, encoding: .utf8) let parentIsNeedle = validateBuilderIsNeedle(builderFilePath: parentBuilderPath) @@ -339,12 +339,12 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: parentBuilderSearchText, with: newName, and: parentName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(parentBuilderPath).write(replacedText) replacedFilePaths.append(parentBuilderPath) } } - + func renameForParentsComponentExtensions() throws { try parents.forEach { parentName in guard let componentExtensionPath = paths.filter({ $0.contains("\(parentName)/Dependencies/\(parentName)Component+\(currentName).swift") }).first else { @@ -352,7 +352,7 @@ private extension RenameCommand { print("Skip to rename codes in \(parentName)Component+\(currentName).swift.".yellow.bold) return } - + print("\t\trename for \(componentExtensionPath.lastElementSplittedBySlash)") let text = try String.init(contentsOfFile: componentExtensionPath, encoding: .utf8) let replacedText = renameSetting.parentComponentExtension.reduce(text) { (result, parentComponentExtensionSearchText) in @@ -360,12 +360,12 @@ private extension RenameCommand { let replaceText = replacePlaceHolder(for: parentComponentExtensionSearchText, with: newName, and: parentName) return result.replacingOccurrences(of: searchText, with: replaceText) } - + try Path(componentExtensionPath).write(replacedText) replacedFilePaths.append(componentExtensionPath) } } - + func formatAllReplacedFiles() throws { print("\n\tStart format for all replaced files.".bold) try replacedFilePaths.forEach { replacedFilePath in @@ -374,10 +374,10 @@ private extension RenameCommand { try Path(replacedFilePath).write(formattedText) } } - + func renameDirectoriesAndFiles() throws { print("\n\tStart renaming directories and files.".bold) - + // for target RIB let targetRIBDirectoryPath = Path(interactorPath).parent() guard targetRIBDirectoryPath.isDirectory else { @@ -389,45 +389,45 @@ private extension RenameCommand { try newDirectoryPath.mkdir() print("\t\tNew directory was created.") print("\t\t\t\(newDirectoryPath.relativePath)".lightBlack) - + let newInteractorPath = Path(newDirectoryPath.description + "/" + interactorPath.lastElementSplittedBySlash.replacingOccurrences(of: currentName, with: newName)) try Path(interactorPath).move(newInteractorPath) print("\t\tInteractor file was renamed and moved to new directory.") print("\t\t\t\(newInteractorPath.relativePath)".lightBlack) - + let newRouterPath = Path(newDirectoryPath.description + "/" + routerPath.lastElementSplittedBySlash.replacingOccurrences(of: currentName, with: newName)) try Path(routerPath).move(newRouterPath) print("\t\tRouter file was renamed and moved to new directory.") print("\t\t\t\(newRouterPath.relativePath)".lightBlack) - + let newBuilderPath = Path(newDirectoryPath.description + "/" + builderPath.lastElementSplittedBySlash.replacingOccurrences(of: currentName, with: newName)) try Path(builderPath).move(newBuilderPath) print("\t\tBuilder file was renamed and moved to new directory.") print("\t\t\t\(newBuilderPath.relativePath)".lightBlack) - + if let viewControllerPath = viewControllerPath { let newViewControllerPath = Path(newDirectoryPath.description + "/" + viewControllerPath.lastElementSplittedBySlash.replacingOccurrences(of: currentName, with: newName)) try Path(viewControllerPath).move(newViewControllerPath) print("\t\tViewController file was renamed and moved to new directory.") print("\t\t\t\(newViewControllerPath.relativePath)".lightBlack) } - + let targetRIBDependenciesDirectoryPath = try Path(targetRIBDirectoryPath.description + "/Dependencies") if targetRIBDependenciesDirectoryPath.exists { let targetRIBDependencyPaths = try targetRIBDependenciesDirectoryPath.children().map { $0.description }.filter { $0.contains("\(currentName)Component+") } - + let newDependenciesDirectoryPath = Path(newDirectoryPath.description + "/Dependencies") try newDependenciesDirectoryPath.mkdir() print("\t\tNew directory was created.") print("\t\t\t\(newDirectoryPath.relativePath)".lightBlack) - + try targetRIBDependencyPaths.forEach { targetRIBDependencyPath in let newDependencyPath = Path(newDependenciesDirectoryPath.description + "/" + targetRIBDependencyPath.lastElementSplittedBySlash.replacingOccurrences(of: "\(currentName)Component+", with: "\(newName)Component+")) try Path(targetRIBDependencyPath).move(newDependencyPath) print("\t\tComponent Extension file was renamed and moved to new directory.") print("\t\t\t\(newDependencyPath.relativePath)".lightBlack) } - + if try targetRIBDependenciesDirectoryPath.children().isEmpty { try targetRIBDependenciesDirectoryPath.delete() print("\t\t\(currentName)/Dependencies directory was removed because its files no longer exists.") @@ -437,13 +437,13 @@ private extension RenameCommand { print("\t\tNot found Dependencies directory, skip to move Component Extension files".yellow) print("\t\t\t\(targetRIBDependenciesDirectoryPath.relativePath)".lightBlack) } - + if try targetRIBDirectoryPath.children().isEmpty { try targetRIBDirectoryPath.delete() print("\t\t\(currentName) directory was removed because its files no longer exists.") print("\t\t\t\(targetRIBDirectoryPath.relativePath)".lightBlack) } - + // for parent RIBs try parents.forEach { parentName in guard let parentInteractorPath = paths.filter({ $0.contains("/" + parentName + "Interactor.swift") }).first else { @@ -456,13 +456,13 @@ private extension RenameCommand { print("Failed to detect target parent RIB \(parentName) directory path.".red.bold) return } - + let parentRIBDependenciesDirectoryPath = try Path(parentRIBDirectoryPath.description + "/Dependencies") guard parentRIBDependenciesDirectoryPath.isDirectory else { print("Failed to detect target parent RIB \(parentName) Dependencies directory path.".red.bold) return } - + let parentRIBDependencyPaths = try parentRIBDependenciesDirectoryPath.children().map { $0.description }.filter { $0.contains("Component+\(currentName).swift") } try parentRIBDependencyPaths.forEach { parentRIBDependencyPath in let newDependencyPath = Path(parentRIBDependenciesDirectoryPath.description + "/" + parentRIBDependencyPath.lastElementSplittedBySlash.replacingOccurrences(of: "Component+\(currentName).swift", with: "Component+\(newName).swift")) @@ -471,7 +471,7 @@ private extension RenameCommand { print("\t\t\t\(newDependencyPath.relativePath)".lightBlack) } } - + } } @@ -482,7 +482,7 @@ private extension RenameCommand { .replacingOccurrences(of: "__RIB_NAME_LOWER_CASED_FIRST_LETTER__", with: ribName.lowercasedFirstLetter()) .replacingOccurrences(of: "__RIB_NAME__", with: ribName) } - + func replacePlaceHolder(for target: String, with ribName: String, and parentRIBName: String) -> String { target .replacingOccurrences(of: "__RIB_NAME_LOWER_CASED_FIRST_LETTER__", with: ribName.lowercasedFirstLetter()) diff --git a/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift b/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift index fad8f3e..47085ac 100644 --- a/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift +++ b/Sources/RIBsCodeGen/Commands/UnlinkCommand.swift @@ -29,7 +29,7 @@ struct UnlinkCommand: Command { self.builderPath = builderPath self.parentBuilderPath = parentBuilderPath } - + func run() -> Result { let builderIsNeedle = validateBuilderIsNeedle(builderFilePath: builderPath) if !builderIsNeedle { @@ -42,7 +42,7 @@ struct UnlinkCommand: Command { print("\nStart unlinking \(targetName) RIB from \(parentName) RIB.".bold) var result: Result? - + if !builderIsNeedle { do { try deleteComponentExtensions(for: parentName) @@ -56,19 +56,19 @@ struct UnlinkCommand: Command { } catch { result = .failure(error: .failedToUnlink("Failed to delete related codes in parent Builder file.")) } - + do { try deleteRelatedCodesInParentRouter(for: parentName) } catch { result = .failure(error: .failedToUnlink("Failed to delete related codes in parent Router file.")) } - + do { try deleteRelatedCodesInParentInteractor(for: parentName) } catch { result = .failure(error: .failedToUnlink("Failed to delete related codes in parent Interactor file.")) } - + return result ?? .success(message: "\nSuccessfully finished unlinking \(targetName) RIB from its parents.".green.bold) } } @@ -82,10 +82,10 @@ private extension UnlinkCommand { print("Not found \(targetFileName). Skip to delete Component Extension files.".yellow.bold) return } - + try delete(path: componentExtensionFilePath) } - + func deleteRelatedCodesInParentBuilder(for parentName: String) throws { print("\n\tDelete related codes in \(parentName)Builder.swift.".bold) let targetFileName = "/\(parentName)/\(parentName)Builder.swift" @@ -93,7 +93,7 @@ private extension UnlinkCommand { print("Not found \(targetFileName.dropFirst()). \(targetName) RIB has already unlinked to \(parentName) RIB.".yellow.bold) return } - + let builderFile = File(path: builderFilePath)! let builderFileStructure = try! Structure(file: builderFile) let builderDictionary = builderFileStructure.dictionary @@ -105,7 +105,7 @@ private extension UnlinkCommand { print("Skip to delete related codes in \(parentName)Builder.swift".yellow.bold) return } - + let inheritedTypes = targetProtocolDictionary.getInheritedTypes() guard !inheritedTypes.isEmpty else { print("No protocols conforms to \(parentName)Dependency.".red.bold) @@ -133,10 +133,10 @@ private extension UnlinkCommand { print("\t\tdelete codes matching with " + "\(searchText)".lightBlack + ".") return result.replacingOccurrences(of: searchText, with: "", options: .regularExpression) } - + try write(text: replacedText, for: builderFilePath) } - + func deleteRelatedCodesInParentRouter(for parentName: String) throws { print("\n\tDelete related codes in \(parentName)Router.swift.".bold) let targetFileName = "/\(parentName)/\(parentName)Router.swift" @@ -144,7 +144,7 @@ private extension UnlinkCommand { print("Not found \(targetFileName.dropFirst()). \(targetName) RIB has already unlinked to \(parentName) RIB.".yellow.bold) return } - + let routerFile = File(path: routerFilePath)! let routerFileStructure = try! Structure(file: routerFile) let routerDictionary = routerFileStructure.dictionary @@ -156,14 +156,14 @@ private extension UnlinkCommand { print("Skip to delete related codes for \(parentName)ViewControllable.".yellow.bold) return } - + let inheritedTypes = targetProtocolDictionary.getInheritedTypes() guard !inheritedTypes.isEmpty else { print("No protocols conforms to \(parentName)ViewControllable.".red.bold) print("Skip to delete related codes for \(parentName)ViewControllable.".yellow.bold) return } - + let text = try String.init(contentsOfFile: routerFilePath, encoding: .utf8) var replacedText: String if inheritedTypes.count == 1 { @@ -172,16 +172,16 @@ private extension UnlinkCommand { } else { replacedText = text } - + replacedText = unlinkSetting.parentRouter.reduce(replacedText) { (result, routerSearchText) in let searchText = replacePlaceHolder(for: routerSearchText, with: targetName, and: parentName) print("\t\tdelete codes matching with " + "\(searchText)".lightBlack + ".") return result.replacingOccurrences(of: searchText, with: "", options: .regularExpression) } - + try write(text: replacedText, for: routerFilePath) } - + func deleteRelatedCodesInParentInteractor(for parentName: String) throws { print("\n\tDelete related codes in \(parentName)Interactor.swift.".bold) let targetFileName = "/\(parentName)/\(parentName)Interactor.swift" @@ -189,25 +189,25 @@ private extension UnlinkCommand { print("Not found \(targetFileName.dropFirst()). \(targetName) RIB has already unlinked to \(parentName) RIB.".yellow.bold) return } - + let text = try String.init(contentsOfFile: interactorFilePath, encoding: .utf8) - + let replacedText = unlinkSetting.parentInteractor.reduce(text) { (result, interactorSearchText) in let searchText = replacePlaceHolder(for: interactorSearchText, with: targetName, and: parentName) print("\t\tdelete codes matching with " + "\(searchText)".lightBlack + ".") return result.replacingOccurrences(of: searchText, with: "", options: .regularExpression) } - + var replacedTextArray = [String]() - replacedText.enumerateLines { line, stop in + replacedText.enumerateLines { line, _ in if line.contains(pattern: "router\\?\\..*\(targetName)\\(") { replacedTextArray.append("// \(targetName) RIB has been deleted. The below codes seem not to be worked.") } replacedTextArray.append(line) } - + let result = replacedTextArray.joined(separator: "\n") + "\n" - + try write(text: result, for: interactorFilePath) } } @@ -217,7 +217,7 @@ extension UnlinkCommand { func delete(path: String) throws { try Path(path).delete() } - + func write(text: String, for path: String) throws { try Path(path).write(text) } @@ -238,6 +238,6 @@ extension String { guard let regex = try? NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options()) else { return false } - return regex.firstMatch(in: self, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, count)) != nil + return regex.firstMatch(in: self, options: NSRegularExpression.MatchingOptions(), range: NSRange(location: 0, length: count)) != nil } } diff --git a/Sources/RIBsCodeGen/Models/Argument.swift b/Sources/RIBsCodeGen/Models/Argument.swift index 30c2f61..730133e 100644 --- a/Sources/RIBsCodeGen/Models/Argument.swift +++ b/Sources/RIBsCodeGen/Models/Argument.swift @@ -58,7 +58,7 @@ enum Action { struct Argument: CustomStringConvertible { let action: Action - let options: [String:String] + let options: [String: String] var description: String { "action: \(action), options: \(options)" diff --git a/Sources/RIBsCodeGen/Models/Formatter.swift b/Sources/RIBsCodeGen/Models/Formatter.swift index 693ceb1..d98748e 100644 --- a/Sources/RIBsCodeGen/Models/Formatter.swift +++ b/Sources/RIBsCodeGen/Models/Formatter.swift @@ -22,7 +22,6 @@ private enum FormatterError: Swift.Error { } } - enum Formatter { static func format(path: String) throws -> String { guard let parentRouterFile = File(path: path) else { diff --git a/Sources/RIBsCodeGen/Models/Shell.swift b/Sources/RIBsCodeGen/Models/Shell.swift index 9101d58..7d7c6a6 100644 --- a/Sources/RIBsCodeGen/Models/Shell.swift +++ b/Sources/RIBsCodeGen/Models/Shell.swift @@ -7,16 +7,16 @@ import Foundation func shell(_ command: String) -> String { let task = Process() let pipe = Pipe() - + task.standardOutput = pipe task.standardError = pipe task.arguments = ["-c", command] task.launchPath = "/bin/zsh" task.standardInput = nil task.launch() - + let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)! - + return output } diff --git a/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift b/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift index 0aeeb57..5a5f7ac 100644 --- a/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift +++ b/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift @@ -266,7 +266,7 @@ private enum SwiftFileFormatter { if trimmedLine.startsTypeDeclaration, let colonIndex = line.firstIndex(of: ":"), - (!trimmedLine.contains("{") || trimmedLine.hasSuffix(",")) { + !trimmedLine.contains("{") || trimmedLine.hasSuffix(",") { let firstInheritedColumn = line.distance(from: line.startIndex, to: colonIndex) + 2 return firstInheritedColumn } diff --git a/Sources/RIBsCodeGen/main.swift b/Sources/RIBsCodeGen/main.swift index 6fc0451..3090741 100644 --- a/Sources/RIBsCodeGen/main.swift +++ b/Sources/RIBsCodeGen/main.swift @@ -144,7 +144,7 @@ func run(with commandLineArguments: [String]) { let parentBuilderClasses = parentBuilderFileStructure.dictionary.getSubStructures().filterByKeyKind(.class) if let parentComponentClass = parentBuilderClasses.filterByKeyName("\(parent)Component").first, - let _ = parentComponentClass.getSubStructures().filterByKeyTypeName("\(targetName)Component").first { + let _ = parentComponentClass.getSubStructures().filterByKeyTypeName("\(targetName)Component").first { return true } else { return false @@ -213,7 +213,7 @@ func analyzeArguments(commandLineArguments: [String]) -> Argument? { let optionArguments = commandLineArgumentsLackOfFirst.dropFirst() - var otherArguments = [String:String]() + var otherArguments = [String: String]() var latestOptionKey = "" optionArguments.forEach { argument in if argument.contains("--") { @@ -254,13 +254,13 @@ func analyzeRenameSettings() -> RenameSetting? { print("Not found setting file, add .ribscodegen_rename at current directory".red.bold) return nil } - + let settingFile: String? = try? settingFilePath.read() guard let settingFileString = settingFile else { print("Failed to read .ribscodegen_rename.".red.bold) return nil } - + let decoder = YAMLDecoder() do { return try decoder.decode(RenameSetting.self, from: settingFileString) @@ -275,13 +275,13 @@ func analyzeUnlinkSettings() -> UnlinkSetting? { print("Not found setting file, add .ribscodegen_unlink at current directory".red.bold) return nil } - + let settingFile: String? = try? settingFilePath.read() guard let settingFileString = settingFile else { print("Failed to read .ribscodegen_unlink.".red.bold) return nil } - + let decoder = YAMLDecoder() do { return try decoder.decode(UnlinkSetting.self, from: settingFileString) From 3cd24c68b1e80d618e9fb8237aa6ab7ad09b932b Mon Sep 17 00:00:00 2001 From: Wataru343 <20026379+Wataru343@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:50:18 +0900 Subject: [PATCH 3/6] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6e4bba8..61b6468 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ +.DS_Store From af40203f6f249eb65911513d2e79666b2a8310b2 Mon Sep 17 00:00:00 2001 From: Wataru343 <20026379+Wataru343@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:50:45 +0900 Subject: [PATCH 4/6] Update .ribscodegen_rename --- .ribscodegen_rename | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.ribscodegen_rename b/.ribscodegen_rename index 02e39ac..21ee069 100644 --- a/.ribscodegen_rename +++ b/.ribscodegen_rename @@ -13,6 +13,7 @@ interactor: - "listener: __RIB_NAME__Listener?" - "presenter: __RIB_NAME__Presentable" - "deactivate__RIB_NAME__" + - "finish__RIB_NAME__" router: - "__RIB_NAME__Router.swift" - "protocol __RIB_NAME__Interactable:" @@ -35,7 +36,7 @@ builder: - "class __RIB_NAME__Builder:" - "__RIB_NAME__Dependency" - "__RIB_NAME__Component" - - "__RIB_NAME__ViewController" + - "__RIB_NAME__ViewController" - "__RIB_NAME_LOWER_CASED_FIRST_LETTER__ViewController" - "__RIB_NAME__ViewControllable" - "__RIB_NAME__Listener" @@ -61,6 +62,7 @@ parentInteractor: - "detach__RIB_NAME__" - "remove__RIB_NAME__" - "deactivate__RIB_NAME__" + - "finish__RIB_NAME__" parentRouter: - " __RIB_NAME__Listener" - "routeTo__RIB_NAME__" @@ -81,7 +83,7 @@ parentNeedleBuilder: - "__RIB_NAME_LOWER_CASED_FIRST_LETTER__Builder: __RIB_NAME_LOWER_CASED_FIRST_LETTER__Builder" - "__RIB_NAME_LOWER_CASED_FIRST_LETTER__Component" - "__RIB_NAME__Component" - parentComponentExtension: +parentComponentExtension: - "__PARENT_RIB_NAME__Component+__RIB_NAME__.swift" - "scope of __PARENT_RIB_NAME__ to provide for the __RIB_NAME__ scope." - "__PARENT_RIB_NAME__Dependency__RIB_NAME__" From 6ccd9c6af2724955cb2539b0edec33ba494b43b7 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 20 Mar 2026 03:54:22 +0900 Subject: [PATCH 5/6] Rename SourceKit extensions for SwiftNode Co-authored-by: Wataru343 <20026379+Wataru343@users.noreply.github.com> --- .../Extensions/{Array+SourceKit.swift => Array+SwiftNode.swift} | 2 +- .../{Dictionary+SourceKit.swift => SwiftNode+Accessors.swift} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Sources/RIBsCodeGen/Extensions/{Array+SourceKit.swift => Array+SwiftNode.swift} (98%) rename Sources/RIBsCodeGen/Extensions/{Dictionary+SourceKit.swift => SwiftNode+Accessors.swift} (98%) diff --git a/Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift b/Sources/RIBsCodeGen/Extensions/Array+SwiftNode.swift similarity index 98% rename from Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift rename to Sources/RIBsCodeGen/Extensions/Array+SwiftNode.swift index 6a7dd0d..25d5a4b 100644 --- a/Sources/RIBsCodeGen/Extensions/Array+SourceKit.swift +++ b/Sources/RIBsCodeGen/Extensions/Array+SwiftNode.swift @@ -1,5 +1,5 @@ // -// Array+SourceKit.swift +// Array+SwiftNode.swift // RIBsCodeGen // // Created by 今入 庸介 on 2021/02/04. diff --git a/Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift b/Sources/RIBsCodeGen/Extensions/SwiftNode+Accessors.swift similarity index 98% rename from Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift rename to Sources/RIBsCodeGen/Extensions/SwiftNode+Accessors.swift index ad23f1c..af49575 100644 --- a/Sources/RIBsCodeGen/Extensions/Dictionary+SourceKit.swift +++ b/Sources/RIBsCodeGen/Extensions/SwiftNode+Accessors.swift @@ -1,5 +1,5 @@ // -// Dictionary+SourceKit.swift +// SwiftNode+Accessors.swift // RIBsCodeGen // // Created by 今入 庸介 on 2021/02/04. From 98e32395ca5246a32851b25dbf849f277aee2de5 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 27 Mar 2026 19:29:25 +0900 Subject: [PATCH 6/6] Fix multiline call indentation in SwiftSyntax formatter Co-authored-by: Wataru343 <20026379+Wataru343@users.noreply.github.com> --- .../Models/SwiftSyntaxSupport.swift | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift b/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift index 5a5f7ac..dc0a7b6 100644 --- a/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift +++ b/Sources/RIBsCodeGen/Models/SwiftSyntaxSupport.swift @@ -143,6 +143,11 @@ private enum LineParser { } private enum SwiftFileFormatter { + private struct ParenthesisIndent { + let continuationIndent: Int + let closingIndent: Int + } + static func format(contents: String, trimmingTrailingWhitespace: Bool, useTabs: Bool, @@ -150,7 +155,7 @@ private enum SwiftFileFormatter { let rawLines = contents.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) var formattedLines: [String] = [] var braceDepth = 0 - var parenStack: [Int] = [] + var parenStack: [ParenthesisIndent] = [] var inheritanceIndent: Int? for rawLine in rawLines { @@ -168,8 +173,10 @@ private enum SwiftFileFormatter { let indent: Int if let inheritanceIndent, !trimmedLine.startsTypeDeclaration { indent = inheritanceIndent - } else if let continuationIndent = parenStack.last, !trimmedLine.hasPrefix("}") { - indent = continuationIndent + } else if trimmedLine.startsWithClosingParenthesis, let parenthesisIndent = parenStack.last { + indent = parenthesisIndent.closingIndent + } else if let parenthesisIndent = parenStack.last, !trimmedLine.hasPrefix("}") { + indent = parenthesisIndent.continuationIndent } else { indent = baseIndent } @@ -179,6 +186,8 @@ private enum SwiftFileFormatter { updateContext( line: normalizedLine, + currentIndent: indent, + indentWidth: indentWidth, braceDepth: &braceDepth, parenStack: &parenStack, inheritanceIndent: &inheritanceIndent @@ -211,8 +220,10 @@ private enum SwiftFileFormatter { } private static func updateContext(line: String, + currentIndent: Int, + indentWidth: Int, braceDepth: inout Int, - parenStack: inout [Int], + parenStack: inout [ParenthesisIndent], inheritanceIndent: inout Int?) { inheritanceIndent = nextInheritanceIndent(current: inheritanceIndent, line: line) @@ -244,7 +255,18 @@ private enum SwiftFileFormatter { case "\"": isInsideString = true case "(": - parenStack.append(index + 1) + let continuationIndent: Int + if hasInlineContentAfterOpenParenthesis(characters: characters, openParenthesisIndex: index) { + continuationIndent = index + 1 + } else { + continuationIndent = currentIndent + indentWidth + } + parenStack.append( + ParenthesisIndent( + continuationIndent: continuationIndent, + closingIndent: currentIndent + ) + ) case ")": if !parenStack.isEmpty { parenStack.removeLast() @@ -261,6 +283,27 @@ private enum SwiftFileFormatter { } } + private static func hasInlineContentAfterOpenParenthesis(characters: [Character], + openParenthesisIndex: Int) -> Bool { + var index = openParenthesisIndex + 1 + + while index < characters.count { + let character = characters[index] + + if character == "/" && index + 1 < characters.count && characters[index + 1] == "/" { + return false + } + + if !character.isWhitespace { + return true + } + + index += 1 + } + + return false + } + private static func nextInheritanceIndent(current: Int?, line: String) -> Int? { let trimmedLine = line.trimmingCharacters(in: .whitespaces) @@ -678,6 +721,10 @@ private extension StringProtocol { } private extension String { + var startsWithClosingParenthesis: Bool { + trimmingCharacters(in: .whitespaces).hasPrefix(")") + } + var startsTypeDeclaration: Bool { let trimmed = trimmingCharacters(in: .whitespaces) return trimmed.hasPrefix("protocol ")