-
Notifications
You must be signed in to change notification settings - Fork 111
Expand file tree
/
Copy pathqueries_transport_boundaries.go
More file actions
209 lines (189 loc) · 7.11 KB
/
Copy pathqueries_transport_boundaries.go
File metadata and controls
209 lines (189 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package graph
import (
"sort"
"strings"
)
// TransportScope represents the set of objects contained in one or more transports.
// Used by AnalyzeTransportBoundaries to determine what's "inside" vs "outside".
type TransportScope struct {
Label string // Display label (TR number, CR ID, etc.)
Transports map[string]bool // TR numbers in scope
Objects map[string]bool // NodeIDs (TYPE:NAME) of objects in scope
names map[string]bool // Object names only (for fuzzy type matching)
}
// InScope returns true if the given node ID is part of the transport scope.
// Checks both full node ID (TYPE:NAME) and name-only, because the parser
// may create nodes with guessed types (TYPE:ZCL_FOO) that differ from the
// canonical TADIR type (CLAS:ZCL_FOO) used in the scope.
func (s *TransportScope) InScope(nodeID string) bool {
if s.Objects[nodeID] {
return true
}
// Lazy-build name index
if s.names == nil {
s.names = make(map[string]bool, len(s.Objects))
for id := range s.Objects {
if parts := strings.SplitN(id, ":", 2); len(parts) == 2 {
s.names[parts[1]] = true
}
}
}
if parts := strings.SplitN(nodeID, ":", 2); len(parts) == 2 {
return s.names[parts[1]]
}
return false
}
// TransportBoundaryEntry represents a dependency that crosses the transport boundary.
type TransportBoundaryEntry struct {
SourceNodeID string `json:"source_node_id"`
SourceName string `json:"source_name"`
SourceType string `json:"source_type"`
SourcePackage string `json:"source_package,omitempty"`
TargetNodeID string `json:"target_node_id"`
TargetName string `json:"target_name"`
TargetType string `json:"target_type"`
TargetPackage string `json:"target_package,omitempty"`
EdgeKind string `json:"edge_kind"`
RefDetail string `json:"ref_detail,omitempty"`
Status string `json:"status"` // MISSING, STANDARD, DYNAMIC
}
// TransportBoundaryReport is the result of a transport boundary analysis.
type TransportBoundaryReport struct {
Scope string `json:"scope"`
ObjectCount int `json:"object_count"`
Missing []TransportBoundaryEntry `json:"missing"` // Custom objects not in transport
Standard []TransportBoundaryEntry `json:"standard"` // SAP standard refs (informational)
Dynamic []TransportBoundaryEntry `json:"dynamic"` // Unresolved dynamic calls
CrossPackage []TransportBoundaryEntry `json:"cross_package"` // In-scope but different package
Summary TransportBoundarySummary `json:"summary"`
}
// TransportBoundarySummary provides counts for the boundary report.
type TransportBoundarySummary struct {
TotalDeps int `json:"total_deps"`
InScope int `json:"in_scope"`
InScopeSamePkg int `json:"in_scope_same_pkg"`
InScopeCrossPkg int `json:"in_scope_cross_pkg"`
Missing int `json:"missing"`
Standard int `json:"standard"`
Dynamic int `json:"dynamic"`
SelfConsistent bool `json:"self_consistent"` // true if Missing == 0
}
// AnalyzeTransportBoundaries checks whether a transport set is self-consistent:
// do the objects it contains depend on anything NOT in the set?
//
// The graph must contain both structural edges (CALLS, REFERENCES, etc.)
// and the transport objects as nodes. Only forward (outgoing) structural
// edges from in-scope objects are analyzed.
//
// Returns a report classifying each external dependency as MISSING (custom,
// not in transport), STANDARD (SAP standard), or DYNAMIC (unresolved).
func AnalyzeTransportBoundaries(g *Graph, scope *TransportScope) *TransportBoundaryReport {
report := &TransportBoundaryReport{
Scope: scope.Label,
ObjectCount: len(scope.Objects),
}
// Track unique source→target pairs to avoid duplicates
type depKey struct{ from, to string }
seen := make(map[depKey]bool)
for nodeID := range scope.Objects {
for _, e := range g.OutEdges(nodeID) {
// Skip transport/co-transport edges — we want structural deps only
if e.Kind == EdgeInTransport || e.Kind == EdgeCoTransported || e.Kind == EdgeReadsConfig {
continue
}
// Skip self-refs
if e.To == nodeID {
continue
}
key := depKey{nodeID, e.To}
if seen[key] {
continue
}
seen[key] = true
report.Summary.TotalDeps++
// In scope — classify as same-package or cross-package
if scope.InScope(e.To) {
report.Summary.InScope++
// Check if it crosses a package boundary within the scope
fromNode := g.GetNode(nodeID)
toNode := g.GetNode(e.To)
if fromNode != nil && toNode != nil && fromNode.Package != "" && toNode.Package != "" && fromNode.Package != toNode.Package {
report.Summary.InScopeCrossPkg++
entry := TransportBoundaryEntry{
SourceNodeID: nodeID,
TargetNodeID: e.To,
SourceName: fromNode.Name,
SourceType: fromNode.Type,
SourcePackage: fromNode.Package,
TargetName: toNode.Name,
TargetType: toNode.Type,
TargetPackage: toNode.Package,
EdgeKind: string(e.Kind),
RefDetail: e.RefDetail,
Status: "CROSS_PACKAGE",
}
report.CrossPackage = append(report.CrossPackage, entry)
} else {
report.Summary.InScopeSamePkg++
}
continue
}
// Build entry
entry := TransportBoundaryEntry{
SourceNodeID: nodeID,
TargetNodeID: e.To,
EdgeKind: string(e.Kind),
RefDetail: e.RefDetail,
}
// Resolve node details
if n := g.GetNode(nodeID); n != nil {
entry.SourceName = n.Name
entry.SourceType = n.Type
entry.SourcePackage = n.Package
}
if n := g.GetNode(e.To); n != nil {
entry.TargetName = n.Name
entry.TargetType = n.Type
entry.TargetPackage = n.Package
}
// Classify
if e.Kind == EdgeDynamic {
entry.Status = "DYNAMIC"
report.Dynamic = append(report.Dynamic, entry)
report.Summary.Dynamic++
} else if IsStandardObject(entry.TargetName) {
entry.Status = "STANDARD"
report.Standard = append(report.Standard, entry)
report.Summary.Standard++
} else {
entry.Status = "MISSING"
report.Missing = append(report.Missing, entry)
report.Summary.Missing++
}
}
}
report.Summary.SelfConsistent = len(report.Missing) == 0
// Sort for stable output
sortEntries := func(entries []TransportBoundaryEntry) {
sort.Slice(entries, func(i, j int) bool {
if entries[i].SourceNodeID != entries[j].SourceNodeID {
return entries[i].SourceNodeID < entries[j].SourceNodeID
}
return entries[i].TargetNodeID < entries[j].TargetNodeID
})
}
sortEntries(report.Missing)
sortEntries(report.Standard)
sortEntries(report.Dynamic)
// Sort cross-package by target package for clean grouping
sort.Slice(report.CrossPackage, func(i, j int) bool {
if report.CrossPackage[i].TargetPackage != report.CrossPackage[j].TargetPackage {
return report.CrossPackage[i].TargetPackage < report.CrossPackage[j].TargetPackage
}
if report.CrossPackage[i].SourcePackage != report.CrossPackage[j].SourcePackage {
return report.CrossPackage[i].SourcePackage < report.CrossPackage[j].SourcePackage
}
return report.CrossPackage[i].SourceNodeID < report.CrossPackage[j].SourceNodeID
})
return report
}