Skip to content

Commit

Permalink
Merge pull request #1 from tisohjung/feature/extension-iflet
Browse files Browse the repository at this point in the history
parse extension iflet
  • Loading branch information
tisohjung authored May 1, 2024
2 parents 49f5978 + 1cc7262 commit 41b0982
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 73 deletions.
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "git@github.com:apple/swift-argument-parser.git",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
"version" : "1.2.0"
Expand All @@ -12,10 +12,10 @@
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "git@github.com:apple/swift-syntax.git",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"branch" : "main",
"revision" : "1d555c8fb517e5e9cee3aca149466bc6a429611b"
"revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd",
"version" : "510.0.1"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let package = Package(
.executable(name: "tca-diagram", targets: ["TCADiagram"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", branch: "main"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
],
targets: [
Expand Down
49 changes: 27 additions & 22 deletions Sources/TCADiagramLib/Diagram.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
import Foundation

import SwiftSyntax
import SwiftParser
import SwiftSyntax

public enum Diagram {
public static func dump(_ sources: [String]) throws -> String {
var relations: [Relation] = [] // Parent ---> Child
var actions: Set<String> = [] // 파일에 존재하는 모든 Action 이름
var pullbackCount: [String: Int] = [:] // 액션이름: pullback count
var relations: [Relation] = [] // Parent ---> Child
var actions: Set<String> = [] // All Action names in file
var pullbackCount: [String: Int] = [:] // [Action name: pullback count]

// 각 소스 파일을 순회하여 actions, relations를 채웁니다.
// go through all files and fill in actions and relations.
try sources.enumerated().forEach { index, source in
print("Parsing... (\(index + 1)/\(sources.count))")
let root: SourceFileSyntax = Parser.parse(source: source)
try root.travel(node: Syntax(root), actions: &actions, relations: &relations)
var reducer = root.description.firstMatch(of: try Regex("Reducer\n.*struct (.*) {"))?[1].substring?
.description ?? ""
if reducer == "" {
reducer = root.description.firstMatch(of: try Regex("\\s+struct (.+?): Reducer"))?[1].substring?
.description ?? ""
}
try root.travel(reducer: reducer, node: Syntax(root), actions: &actions, relations: &relations)
}

return Array
.init(
[
"```mermaid",
Self.mermaidHeader,
Self.relationSection(relations: relations, actions: actions, pullbackCount: &pullbackCount),
"",
Self.idSection(pullbackCount: pullbackCount),
"```"
]
)
.joined(separator: "\n")
return Array(
[
"```mermaid",
Self.mermaidHeader,
Self.relationSection(relations: relations, actions: actions, pullbackCount: &pullbackCount),
"",
Self.idSection(pullbackCount: pullbackCount),
"```"
]
)
.joined(separator: "\n")
}

private static let mermaidHeader = """
Expand All @@ -42,15 +47,15 @@ public enum Diagram {
) -> String {
relations
.sorted(
// parent 먼저 정렬 후 child를 정렬합니다.
// order parent first, then child.
using: [
KeyPathComparator(\.parent, order: .forward),
KeyPathComparator(\.child, order: .forward),
]
)
.map { (relation: Relation) -> Relation in
// AITutor, aiTutor, AiTutor와 같은 문제를 해결하기 위해
// 실제 정의된 Action 이름에서 같은 이름이 있다면 Action 이름으로 대체합니다.
// to fix case problems like AITutor, aiTutor, AiTutor,
// if there is defined Action, use that name.
if let action = actions.first(where: { action in action.lowercased() == relation.child.lowercased() }) {
pullbackCount[action] = (pullbackCount[action] ?? 0) + 1
return Relation(
Expand All @@ -75,7 +80,7 @@ public enum Diagram {
) -> String {
pullbackCount
.sorted(by: { $0.0 < $1.0 })
.map { (key, value) in "\(key)(\(key): \(value))".indent }
.map { key, value in "\(key)(\(key): \(value))".indent }
.joined(separator: "\n")
}
}
127 changes: 87 additions & 40 deletions Sources/TCADiagramLib/Internal/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import SwiftSyntax

extension SourceFileSyntax {
func travel(
reducer: String = "",
node: Syntax,
actions: inout Set<String>,
relations: inout [Relation]
) throws {

if let reducerProtocolParent = try predicateReducerProtocol(node) {
try travel(parent: reducerProtocolParent, node: node, actions: &actions, relations: &relations)
}
Expand All @@ -22,12 +22,23 @@ extension SourceFileSyntax {
} else if let name = try predicateActionDecl(node) {
actions.insert(name)
} else {
for child in node.children(viewMode: .all) {
try travel(
node: child,
actions: &actions,
relations: &relations
)
if reducer.isEmpty {
for child in node.children(viewMode: .all) {
try travel(
node: child,
actions: &actions,
relations: &relations
)
}
} else {
for child in node.children(viewMode: .all) {
try travel(
parent: reducer,
node: child,
actions: &actions,
relations: &relations
)
}
}
}
}
Expand All @@ -42,7 +53,17 @@ extension SourceFileSyntax {
actions: inout Set<String>,
relations: inout [Relation]
) throws {
if let (children, isOptional) = try predicateChildReducerProtocol(node) {
if let children = try predicateIfLetDecl(node) {
children.forEach { child in
relations.append(
.init(
parent: parent,
child: child.firstUppercased,
optional: true
)
)
}
} else if let (children, isOptional) = try predicateChildReducerProtocol(node) {
children.forEach { child in
relations.append(
.init(
Expand All @@ -66,22 +87,21 @@ extension SourceFileSyntax {
}

extension SourceFileSyntax {

/// Get parent name from feature.
private func predicateReducerProtocol(_ node: Syntax) throws -> String? {
if
let node = StructDeclSyntax(node)
{
/// Has @Reducer macro
if
node.attributes?.contains(where: { element in
node.attributes.contains(where: { element in
element.tokens(viewMode: .fixedUp).contains { el in
el.tokenKind == .identifier("Reducer")
}
}) == true
{
debugPrint(node.identifier.text)
return node.identifier.text
debugPrint(node.name.text)
return node.name.text
}
/// superclass of ReducerProtocol or Reducer
if
Expand All @@ -91,7 +111,7 @@ extension SourceFileSyntax {
|| $0.tokenKind == .identifier("Reducer")
}) == true
{
return node.identifier.text
return node.name.text
}
}
return nil
Expand All @@ -101,7 +121,7 @@ extension SourceFileSyntax {
private func predicateChildReducerProtocol(_ node: Syntax) throws -> ([String], Bool)? {
if
let node = FunctionCallExprSyntax(node),
node.argumentList.contains(where: { syntax in syntax.label?.text == "action" })
node.arguments.contains(where: { syntax in syntax.label?.text == "action" })
{
if
node.tokens(viewMode: .fixedUp).contains(where: { $0.tokenKind == .identifier("Scope") }),
Expand Down Expand Up @@ -140,31 +160,31 @@ extension SourceFileSyntax {
private func predicatePullbackCall(_ node: Syntax) throws -> (FunctionCallExprSyntax, String, String)? {
if
let node = FunctionCallExprSyntax(node),
let action = node.argumentList.first(where: { syntax in syntax.label?.text == "action" })?.expression
let action = node.arguments.first(where: { syntax in syntax.label?.text == "action" })?.expression
{
let child = node.description.firstMatch(of: try Regex("\\s+(.+?)Reducer"))?[1].substring?.description
let parent = "\(action)".firstMatch(of: try Regex("\\/(.+?)Action.+"))?[1].substring?.description
switch (child, parent) {
case (.some("Any"), .some(let parent)):
if
let child = node.description
.firstMatch(of: try Regex("(?s)\\s+AnyReducer.*\\{.+?\\s+(\\w+?)\\("))?[1]
.substring?
.description,
node.tokens(viewMode: .fixedUp).map(\.text).contains("pullback")
{
return (node, parent, child)
}
return .none
if
let child = node.description.firstMatch(of: try Regex("\\s+(.+?)Reducer"))?[1].substring?.description,
let parent = "\(action)".firstMatch(of: try Regex("\\/(.+?)Action.+"))?[1].substring?.description
{
switch (child, parent) {
case ("Any", let parent):
if
let child = node.description
.firstMatch(of: try Regex("(?s)\\s+AnyReducer.*\\{.+?\\s+(\\w+?)\\("))?[1]
.substring?
.description,
node.tokens(viewMode: .fixedUp).map(\.text).contains("pullback")
{
return (node, parent, child)
}
return .none

case (.some(let child), .some(let parent)):
if node.tokens(viewMode: .fixedUp).map(\.text).contains("pullback") {
return (node, parent, child)
case (let child, let parent):
if node.tokens(viewMode: .fixedUp).map(\.text).contains("pullback") {
return (node, parent, child)
}
return .none
}
return .none

default:
return .none
}
}
return .none
Expand All @@ -173,13 +193,13 @@ extension SourceFileSyntax {
/// parse `enum` Action for feature name.
private func predicateActionDecl(_ node: Syntax) throws -> String? {
if let node = EnumDeclSyntax(node) {
if node.identifier.text == "Action" {
if node.name.text == "Action" {
var parent = node.parent
while parent != nil {
if
let ext = ExtensionDeclSyntax(parent),
let name = ext.children(viewMode: .fixedUp)
.compactMap(SimpleTypeIdentifierSyntax.init)
.compactMap(IdentifierTypeSyntax.init)
.first?
.name
.text
Expand All @@ -190,8 +210,8 @@ extension SourceFileSyntax {
}
}
return .none
} else if node.identifier.text.hasSuffix("Action") {
return node.identifier.text.replacing("Action", with: "")
} else if node.name.text.hasSuffix("Action") {
return node.name.text.replacing("Action", with: "")
} else {
return .none
}
Expand All @@ -214,4 +234,31 @@ extension SourceFileSyntax {
}
return false
}

/// parse `enum` Action for feature name.
private func predicateIfLetDecl(_ node: Syntax) throws -> [String]? {
// let parent = "\(action)".firstMatch(of: try Regex("\\/(.+?)Action.+"))?[1].substring?.description {
if
let node = FunctionCallExprSyntax(node),
node.arguments.contains(where: { syntax in syntax.label?.text == "action" })
{

// ifLet can be in "method chaining"
// therefore find all reducer names that match and save in child
if
node.tokens(viewMode: .fixedUp).contains(where: { $0.tokenKind == .identifier("ifLet") })
{
let children = node.description
.matches(of: try Regex("ifLet.+{\\s+(.+?)\\(\\)"))
.compactMap {
$0[1].substring?.description
}
.filter {
$0 != "EmptyReducer"
}
return children
}
}
return .none
}
}
4 changes: 2 additions & 2 deletions Sources/TCADiagramLib/Internal/StringProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
extension StringProtocol {
/// 제일 처음 문자를 대문자로 변경합니다.
/// uppercase first letter
var firstUppercased: String {
prefix(1).uppercased() + dropFirst()
}

/// 문자열 앞에 4개의 공백을 추가합니다.
/// add 4 space indent as prefix
var indent: String {
" \(self)"
}
Expand Down
22 changes: 22 additions & 0 deletions Tests/TCADiagramLibTests/DiagramTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,26 @@ final class DiagramTests: XCTestCase {
"""
XCTAssertEqual(result, expected)
}

func testreducerWithExtensionSampleSource() throws {
let result = try Diagram.dump(reducerWithExtensionSampleSource)
let expected = """
```mermaid
%%{ init : { "theme" : "default", "flowchart" : { "curve" : "monotoneY" }}}%%
graph LR
SelfLessonDetail -- optional --> DoubleIfLetChild
SelfLessonDetail ---> DoubleScopeChild
SelfLessonDetail ---> Payment
SelfLessonDetail -- optional --> SantaWeb
SelfLessonDetail -- optional --> SelfLessonDetailFilter
DoubleIfLetChild(DoubleIfLetChild: 1)
DoubleScopeChild(DoubleScopeChild: 1)
Payment(Payment: 1)
SantaWeb(SantaWeb: 1)
SelfLessonDetailFilter(SelfLessonDetailFilter: 1)
```
"""
XCTAssertEqual(result, expected)
}
}
Loading

0 comments on commit 41b0982

Please sign in to comment.