Skip to content

Commit 04874d5

Browse files
authored
Merge pull request #19 from rofle100lvl/ExtensionsSupprot
Added extension support in search
2 parents 8e27fab + 4f17808 commit 04874d5

File tree

14 files changed

+671
-47
lines changed

14 files changed

+671
-47
lines changed

Sources/UseGraphPeriphery/Data/Extensions/Declaration+Extensions.swift

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,69 @@ extension Declaration {
2121
}
2222
return parent
2323
}
24+
25+
private func isExtensionKind(_ kind: Declaration.Kind) -> Bool {
26+
return kind == .extension ||
27+
kind == .extensionEnum ||
28+
kind == .extensionStruct ||
29+
kind == .extensionClass ||
30+
kind == .extensionProtocol
31+
}
32+
33+
private func findParentExtension() -> Declaration? {
34+
var parent: Declaration? = self.parent
35+
while let currentParent = parent {
36+
if isExtensionKind(currentParent.kind) {
37+
return currentParent
38+
}
39+
parent = currentParent.parent
40+
}
41+
return nil
42+
}
43+
44+
private func extractExtensionInfo(from extensionDecl: Declaration, sourceGraph: SourceGraph) -> String? {
45+
// Find the extended type in the extensions dictionary
46+
for (extendedDecl, extensionSet) in sourceGraph.extensions {
47+
if extensionSet.contains(extensionDecl) {
48+
return "extension:\(extendedDecl.name ?? "Unknown"):\(extensionDecl.kind.rawValue):\(extensionDecl.location.line)"
49+
}
50+
}
51+
52+
// If not found in dictionary, use the extension's own name
53+
return "extension:\(extensionDecl.name ?? "Unknown"):\(extensionDecl.kind.rawValue):\(extensionDecl.location.line)"
54+
}
55+
56+
private func findExtensionByLocation(for declaration: Declaration, in sourceGraph: SourceGraph) -> Declaration? {
57+
guard let parent = declaration.parent else { return nil }
58+
59+
// Check if parent has extensions
60+
guard let extensionSet = sourceGraph.extensions[parent] else { return nil }
61+
62+
let declFile = declaration.location.file.path.string
63+
let declLine = declaration.location.line
64+
65+
// Find extension in the same file that starts before the declaration
66+
var bestMatch: Declaration? = nil
67+
var bestMatchLine = 0
68+
69+
for ext in extensionSet {
70+
let extFile = ext.location.file.path.string
71+
let extLine = ext.location.line
72+
73+
// Extension must be in the same file and start before the declaration
74+
if extFile == declFile && extLine < declLine && extLine > bestMatchLine {
75+
bestMatch = ext
76+
bestMatchLine = extLine
77+
}
78+
}
79+
80+
return bestMatch
81+
}
2482

25-
func presentAsNode() -> Node? {
83+
func presentAsNode(sourceGraph: SourceGraph? = nil) -> Node? {
2684
let entity = findEntity()
2785
guard let entity else { return nil }
2886

29-
// Безопасное извлечение модуля
3087
let moduleName = entity.location.file.modules.first ?? "UnknownModule"
3188

3289
return Node(
@@ -39,4 +96,16 @@ extension Declaration {
3996
usrs: entity.usrs
4097
)
4198
}
99+
100+
func getExtensionInfo(sourceGraph: SourceGraph) -> String? {
101+
// First try to find extension through parent hierarchy
102+
if let parentExtension = findParentExtension() {
103+
return extractExtensionInfo(from: parentExtension, sourceGraph: sourceGraph)
104+
}
105+
// If not found through parent, try to find by location
106+
else if let extensionByLocation = findExtensionByLocation(for: self, in: sourceGraph) {
107+
return extractExtensionInfo(from: extensionByLocation, sourceGraph: sourceGraph)
108+
}
109+
return nil
110+
}
42111
}

Sources/UseGraphPeriphery/Data/Repositories/SourceGraphRepository.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public final class SourceGraphRepository: SourceGraphRepositoryProtocol {
2121

2222
guard let entity = reference.parent?.findEntity(),
2323
entity != declaration.findEntity(),
24-
let entityParent = entity.presentAsNode(),
25-
let declarationParent = declaration.presentAsNode() else { return }
24+
let entityParent = entity.presentAsNode(sourceGraph: sourceGraph),
25+
let declarationParent = declaration.presentAsNode(sourceGraph: sourceGraph) else { return }
2626

2727
let edge: EdgeWithoutReference
2828

@@ -38,13 +38,17 @@ public final class SourceGraphRepository: SourceGraphRepositoryProtocol {
3838
)
3939
}
4040

41+
// Получаем extensionInfo для declaration (вызываемого метода/свойства)
42+
let extensionInfo = declaration.getExtensionInfo(sourceGraph: sourceGraph)
43+
4144
if edgeDict[edge] == nil {
4245
edgeDict[edge] = []
4346
}
4447
edgeDict[edge]?.append(
4548
Reference(
4649
line: reference.location.line,
47-
file: reference.location.file.path.string
50+
file: reference.location.file.path.string,
51+
extensionInfo: extensionInfo
4852
)
4953
)
5054
}

Sources/UseGraphPeriphery/Data/Services/GraphOutputService.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,29 @@ public final class GraphOutputService: GraphOutputServiceProtocol {
5858
let edgesCSV = csvBuilder.createCSV(from: coreEdges)
5959
let nodesCSV = csvBuilder.createCSV(from: Array(uniqueSet))
6060

61+
// Collect all references from all edges
62+
var allReferences: [Reference] = []
63+
for edge in edges {
64+
allReferences.append(contentsOf: edge.references)
65+
}
66+
let referencesCSV = csvBuilder.createCSV(from: allReferences)
67+
6168
let nodesUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
6269
.appending(path: "Nodes.csv")
6370
let edgesUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
6471
.appending(path: "Edges.csv")
72+
let referencesUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
73+
.appending(path: "References.csv")
6574

6675
guard let edgesData = edgesCSV.data(using: .utf8),
67-
let nodesData = nodesCSV.data(using: .utf8) else {
76+
let nodesData = nodesCSV.data(using: .utf8),
77+
let referencesData = referencesCSV.data(using: .utf8) else {
6878
throw OutputFormatError.formatIsNotCorrect
6979
}
7080

7181
FileManager.default.createFile(atPath: edgesUrl.path(), contents: edgesData)
7282
FileManager.default.createFile(atPath: nodesUrl.path(), contents: nodesData)
83+
FileManager.default.createFile(atPath: referencesUrl.path(), contents: referencesData)
7384
}
7485

7586
private func mapToGraphVizFormat(format: OutputFormat) -> Format? {
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
import Foundation
2+
import Utils
23

3-
public struct Reference: Hashable, Comparable {
4+
public struct Reference: Hashable, Comparable, CSVRepresentable {
45
public static func < (lhs: Reference, rhs: Reference) -> Bool {
56
lhs.file < rhs.file || lhs.line < rhs.line
67
}
78

89
public let line: Int
910
public let file: String
11+
public let extensionInfo: String?
1012

11-
public init(line: Int, file: String) {
13+
public init(line: Int, file: String, extensionInfo: String? = nil) {
1214
self.line = line
1315
self.file = file
16+
self.extensionInfo = extensionInfo
17+
}
18+
19+
// CSVRepresentable
20+
public var csvRepresentation: String {
21+
let fields = [
22+
String(line),
23+
file,
24+
extensionInfo ?? ""
25+
]
26+
return fields.joined(separator: ",")
27+
}
28+
29+
public var fields: [String] {
30+
return ["line", "file", "extensionInfo"]
1431
}
1532
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import XCTest
2+
@testable import UseGraphPeriphery
3+
4+
enum ExtensionTestHelpers {
5+
6+
/// Проверяет extensionInfo в reference и возвращает распарсенные компоненты
7+
static func validateExtensionInfo(
8+
_ extensionInfo: String,
9+
edge: UseGraphPeriphery.Edge,
10+
file: StaticString = #file,
11+
line: UInt = #line
12+
) -> (extendedType: String, extensionKind: String, extensionLine: String)? {
13+
14+
// Проверяем формат
15+
XCTAssertTrue(extensionInfo.contains("extension:"),
16+
"Extension info should start with 'extension:'",
17+
file: file, line: line)
18+
19+
// Парсим extensionInfo: "extension:<Type>:<Kind>:<Line>"
20+
let components = extensionInfo.split(separator: ":")
21+
XCTAssertEqual(components.count, 4,
22+
"Extension info should have 4 components: \(extensionInfo)",
23+
file: file, line: line)
24+
25+
guard components.count == 4 else { return nil }
26+
27+
let extendedType = String(components[1])
28+
let extensionKind = String(components[2])
29+
let extensionLine = String(components[3])
30+
31+
// Проверяем, что extended type соответствует target в edge
32+
XCTAssertEqual(edge.to.entityName, extendedType,
33+
"Extended type '\(extendedType)' in extensionInfo should match edge target '\(edge.to.entityName ?? "nil")'",
34+
file: file, line: line)
35+
36+
// Проверяем, что extension kind корректный
37+
XCTAssertTrue(extensionKind.contains("extension"),
38+
"Extension kind '\(extensionKind)' should contain 'extension'",
39+
file: file, line: line)
40+
41+
return (extendedType, extensionKind, extensionLine)
42+
}
43+
44+
/// Проверяет конкретные значения extensionInfo для известного типа
45+
static func assertExtensionInfo(
46+
_ extensionInfo: String,
47+
extendedType expectedType: String,
48+
extensionKind expectedKind: String,
49+
extensionLine expectedLine: String,
50+
file: StaticString = #file,
51+
line: UInt = #line
52+
) {
53+
let components = extensionInfo.split(separator: ":")
54+
guard components.count == 4 else {
55+
XCTFail("Invalid extensionInfo format: \(extensionInfo)", file: file, line: line)
56+
return
57+
}
58+
59+
let extendedType = String(components[1])
60+
let extensionKind = String(components[2])
61+
let extensionLine = String(components[3])
62+
63+
XCTAssertEqual(extendedType, expectedType,
64+
"Extended type should be '\(expectedType)'",
65+
file: file, line: line)
66+
XCTAssertEqual(extensionKind, expectedKind,
67+
"Extension kind should be '\(expectedKind)'",
68+
file: file, line: line)
69+
XCTAssertEqual(extensionLine, expectedLine,
70+
"Extension line should be '\(expectedLine)'",
71+
file: file, line: line)
72+
}
73+
74+
/// Собирает все references с extensionInfo из edges
75+
static func collectReferencesWithExtension(
76+
from edges: [UseGraphPeriphery.Edge]
77+
) -> [(edge: UseGraphPeriphery.Edge, ref: UseGraphPeriphery.Reference)] {
78+
var result: [(edge: UseGraphPeriphery.Edge, ref: UseGraphPeriphery.Reference)] = []
79+
80+
for edge in edges {
81+
for ref in edge.references {
82+
if ref.extensionInfo != nil {
83+
result.append((edge, ref))
84+
}
85+
}
86+
}
87+
88+
return result
89+
}
90+
91+
/// Выводит детальную информацию о references с extensionInfo
92+
static func printExtensionReferences(
93+
_ references: [(edge: UseGraphPeriphery.Edge, ref: UseGraphPeriphery.Reference)]
94+
) {
95+
print("\n📍 Found \(references.count) references with extensionInfo:")
96+
for (index, item) in references.enumerated() {
97+
print(" [\(index)] \(item.edge.from.entityName ?? "?") -> \(item.edge.to.entityName ?? "?")")
98+
print(" Line: \(item.ref.line), extensionInfo: \(item.ref.extensionInfo ?? "nil")")
99+
100+
if let extInfo = item.ref.extensionInfo,
101+
let parsed = validateExtensionInfo(extInfo, edge: item.edge) {
102+
print(" Parsed: type=\(parsed.extendedType), kind=\(parsed.extensionKind), line=\(parsed.extensionLine)")
103+
}
104+
}
105+
}
106+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Source,Target,Type
2+
MyLibrary.B.class.s:9MyLibrary1BC@/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift:9,MyLibrary.A.struct.s:9MyLibrary1AV@/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift:3,directed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
id,moduleName,fileName,line,entityName,entityType
2+
MyLibrary.B.class.s:9MyLibrary1BC@/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift:9,MyLibrary,/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift,9,B,class
3+
MyLibrary.A.struct.s:9MyLibrary1AV@/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift:3,MyLibrary,/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift,3,A,struct
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
line,file,extensionInfo
2+
10,/Users/rofle100lvl/Desktop/UseGraph/Tests/UseGraphPeripheryTests/TestProject/MyLibrary/Sources/MyLibrary/UserRepository.swift,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version: 5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyProtocolLibrary",
6+
products: [
7+
.library(
8+
name: "MyProtocolLibrary",
9+
targets: ["MyProtocolLibrary"]),
10+
],
11+
targets: [
12+
.target(
13+
name: "MyProtocolLibrary"),
14+
]
15+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
3+
// Базовый протокол
4+
public protocol Describable {
5+
var name: String { get }
6+
}
7+
8+
// Extension добавляет computed property и метод к протоколу
9+
public extension Describable {
10+
var fullDescription: String {
11+
return "Description: \(name)"
12+
}
13+
14+
func printDescription() {
15+
print(fullDescription)
16+
}
17+
}
18+
19+
// Struct, который реализует протокол
20+
public struct Product: Describable {
21+
public let name: String
22+
public let price: Double
23+
24+
public init(name: String, price: Double) {
25+
self.name = name
26+
self.price = price
27+
}
28+
}
29+
30+
// Extension для struct с computed property
31+
public extension Product {
32+
var priceDescription: String {
33+
return "Price: $\(price)"
34+
}
35+
36+
var isExpensive: Bool {
37+
return price > 100
38+
}
39+
}
40+
41+
// Класс, который использует методы и свойства из extensions
42+
public class ProductManager {
43+
public init() {}
44+
45+
public func displayProduct(_ product: Product) -> String {
46+
let desc = product.fullDescription // Из protocol extension
47+
let price = product.priceDescription // Из struct extension
48+
let expensive = product.isExpensive ? "Expensive" : "Affordable" // Из struct extension
49+
return "\(desc), \(price), \(expensive)"
50+
}
51+
52+
public func printProduct(_ product: Product) {
53+
product.printDescription() // Из protocol extension
54+
}
55+
}

0 commit comments

Comments
 (0)