@ctxo/lang-go 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,332 @@
1
+ // Package reach builds an SSA program, computes a Class Hierarchy Analysis
2
+ // (CHA) callgraph, and returns the set of function/method symbols reachable
3
+ // from a set of roots (main.main + init if binary; all exported API if
4
+ // library).
5
+ //
6
+ // A reflect-safe post-pass grafts back methods on types that appear as
7
+ // arguments to reflect.TypeOf / reflect.ValueOf / reflect.New or
8
+ // json.Marshal/Unmarshal — the most common source of false-positive dead
9
+ // code in Go codebases.
10
+ //
11
+ // Design note: ADR-013 originally specified RTA, but RTA panics on generic
12
+ // code (golang/go issue). CHA is conservative (over-approximates reachability
13
+ // through interface dispatch) which is preferable for dead-code detection —
14
+ // it reduces false positives at the cost of slightly less precise liveness.
15
+ // Analysis runs with a deadline per ADR-013 §4 Q3.
16
+ package reach
17
+
18
+ import (
19
+ "go/ast"
20
+ "go/token"
21
+ "go/types"
22
+ "path/filepath"
23
+ "strings"
24
+ "time"
25
+
26
+ "golang.org/x/tools/go/callgraph"
27
+ "golang.org/x/tools/go/callgraph/cha"
28
+ "golang.org/x/tools/go/packages"
29
+ "golang.org/x/tools/go/ssa"
30
+ "golang.org/x/tools/go/ssa/ssautil"
31
+
32
+ "github.com/alperhankendi/ctxo/tools/ctxo-go-analyzer/internal/symbols"
33
+ )
34
+
35
+ // Result is the reachability snapshot. Reachable holds symbol IDs that are
36
+ // transitively callable from the chosen roots. HasMain distinguishes binary
37
+ // mode (precise) from library mode (approximated via exported API).
38
+ type Result struct {
39
+ Reachable map[string]bool
40
+ HasMain bool
41
+ Timeout bool
42
+ Elapsed time.Duration
43
+ }
44
+
45
+ // Analyze runs SSA + RTA against the loaded package set. deadline bounds the
46
+ // RTA phase specifically (SSA build is always completed — it is cheap).
47
+ func Analyze(rootDir string, pkgs []*packages.Package, deadline time.Duration) *Result {
48
+ start := time.Now()
49
+
50
+ prog, _ := ssautil.AllPackages(pkgs, ssa.BuilderMode(0))
51
+ prog.Build()
52
+
53
+ pkgByTypes := make(map[*types.Package]*packages.Package)
54
+ packages.Visit(pkgs, nil, func(p *packages.Package) {
55
+ if p.Types != nil {
56
+ pkgByTypes[p.Types] = p
57
+ }
58
+ })
59
+
60
+ roots, hasMain := collectRoots(prog, pkgs, rootDir)
61
+
62
+ result := &Result{Reachable: map[string]bool{}, HasMain: hasMain}
63
+
64
+ if len(roots) > 0 {
65
+ done := make(chan *callgraph.Graph, 1)
66
+ go func() {
67
+ // cha.CallGraph is synchronous and not cancellable — on timeout the
68
+ // goroutine leaks but the process exits shortly after.
69
+ done <- cha.CallGraph(prog)
70
+ }()
71
+
72
+ select {
73
+ case cg := <-done:
74
+ for fn := range reachableFromGraph(cg, roots) {
75
+ if id, ok := ssaFuncSymbolID(fn, rootDir); ok {
76
+ result.Reachable[id] = true
77
+ }
78
+ }
79
+ case <-time.After(deadline):
80
+ result.Timeout = true
81
+ }
82
+ }
83
+
84
+ applyReflectSafe(pkgs, pkgByTypes, rootDir, result.Reachable)
85
+
86
+ result.Elapsed = time.Since(start)
87
+ return result
88
+ }
89
+
90
+ // reachableFromGraph performs a BFS over the callgraph starting from the
91
+ // provided root functions and returns the set of reachable *ssa.Function.
92
+ func reachableFromGraph(cg *callgraph.Graph, roots []*ssa.Function) map[*ssa.Function]struct{} {
93
+ seen := make(map[*ssa.Function]struct{})
94
+ queue := make([]*ssa.Function, 0, len(roots))
95
+ for _, r := range roots {
96
+ if r != nil {
97
+ queue = append(queue, r)
98
+ }
99
+ }
100
+ for len(queue) > 0 {
101
+ fn := queue[0]
102
+ queue = queue[1:]
103
+ if _, ok := seen[fn]; ok {
104
+ continue
105
+ }
106
+ seen[fn] = struct{}{}
107
+ node := cg.Nodes[fn]
108
+ if node == nil {
109
+ continue
110
+ }
111
+ for _, edge := range node.Out {
112
+ if edge.Callee != nil && edge.Callee.Func != nil {
113
+ queue = append(queue, edge.Callee.Func)
114
+ }
115
+ }
116
+ }
117
+ return seen
118
+ }
119
+
120
+ // collectRoots returns the CHA entry set. Binary packages contribute their
121
+ // main + init; when no main exists anywhere, every exported function/method
122
+ // in the module becomes a root (library approximation).
123
+ func collectRoots(prog *ssa.Program, pkgs []*packages.Package, rootDir string) ([]*ssa.Function, bool) {
124
+ var mains []*ssa.Function
125
+ var inits []*ssa.Function
126
+ packages.Visit(pkgs, nil, func(p *packages.Package) {
127
+ sp := prog.Package(p.Types)
128
+ if sp == nil {
129
+ return
130
+ }
131
+ if p.Name == "main" {
132
+ if main := sp.Func("main"); main != nil {
133
+ mains = append(mains, main)
134
+ }
135
+ }
136
+ if init := sp.Func("init"); init != nil {
137
+ inits = append(inits, init)
138
+ }
139
+ })
140
+
141
+ if len(mains) > 0 {
142
+ return append(mains, inits...), true
143
+ }
144
+
145
+ // Library mode — treat every module-local exported function/method as a root.
146
+ var roots []*ssa.Function
147
+ roots = append(roots, inits...)
148
+ packages.Visit(pkgs, nil, func(p *packages.Package) {
149
+ if !inModule(p, rootDir) {
150
+ return
151
+ }
152
+ sp := prog.Package(p.Types)
153
+ if sp == nil {
154
+ return
155
+ }
156
+ for _, member := range sp.Members {
157
+ switch m := member.(type) {
158
+ case *ssa.Function:
159
+ if isExported(m.Name()) {
160
+ roots = append(roots, m)
161
+ }
162
+ case *ssa.Type:
163
+ // Methods on a named type live on the method set, not the member map.
164
+ mset := prog.MethodSets.MethodSet(m.Type())
165
+ for i := 0; i < mset.Len(); i++ {
166
+ fn := prog.MethodValue(mset.At(i))
167
+ if fn != nil && isExported(fn.Name()) {
168
+ roots = append(roots, fn)
169
+ }
170
+ }
171
+ // Also pointer receiver methods.
172
+ ptrSet := prog.MethodSets.MethodSet(types.NewPointer(m.Type()))
173
+ for i := 0; i < ptrSet.Len(); i++ {
174
+ fn := prog.MethodValue(ptrSet.At(i))
175
+ if fn != nil && isExported(fn.Name()) {
176
+ roots = append(roots, fn)
177
+ }
178
+ }
179
+ }
180
+ }
181
+ })
182
+ return roots, false
183
+ }
184
+
185
+ // applyReflectSafe grafts method symbols of reflect-accessed types back into
186
+ // the reachable set. Scope is conservative: reflect.TypeOf, reflect.ValueOf,
187
+ // reflect.New, and json.Unmarshal / json.NewDecoder arguments.
188
+ func applyReflectSafe(pkgs []*packages.Package, pkgByTypes map[*types.Package]*packages.Package, rootDir string, reachable map[string]bool) {
189
+ safe := make(map[*types.Named]struct{})
190
+ for _, p := range pkgs {
191
+ if p.TypesInfo == nil {
192
+ continue
193
+ }
194
+ for _, f := range p.Syntax {
195
+ ast.Inspect(f, func(n ast.Node) bool {
196
+ call, ok := n.(*ast.CallExpr)
197
+ if !ok {
198
+ return true
199
+ }
200
+ if !isReflectOrJSONCall(call, p.TypesInfo) {
201
+ return true
202
+ }
203
+ if len(call.Args) == 0 {
204
+ return true
205
+ }
206
+ t := p.TypesInfo.TypeOf(call.Args[0])
207
+ if t == nil {
208
+ return true
209
+ }
210
+ if ptr, ok := t.(*types.Pointer); ok {
211
+ t = ptr.Elem()
212
+ }
213
+ if named, ok := t.(*types.Named); ok {
214
+ safe[named] = struct{}{}
215
+ }
216
+ return true
217
+ })
218
+ }
219
+ }
220
+
221
+ for named := range safe {
222
+ addMethodsToReachable(named, pkgByTypes, rootDir, reachable)
223
+ }
224
+ }
225
+
226
+ func isReflectOrJSONCall(call *ast.CallExpr, info *types.Info) bool {
227
+ sel, ok := call.Fun.(*ast.SelectorExpr)
228
+ if !ok {
229
+ return false
230
+ }
231
+ x, ok := sel.X.(*ast.Ident)
232
+ if !ok {
233
+ return false
234
+ }
235
+ obj := info.Uses[x]
236
+ pn, ok := obj.(*types.PkgName)
237
+ if !ok {
238
+ return false
239
+ }
240
+ path := pn.Imported().Path()
241
+ switch path {
242
+ case "reflect":
243
+ switch sel.Sel.Name {
244
+ case "TypeOf", "ValueOf", "New":
245
+ return true
246
+ }
247
+ case "encoding/json":
248
+ switch sel.Sel.Name {
249
+ case "Unmarshal", "NewDecoder", "Marshal", "NewEncoder":
250
+ return true
251
+ }
252
+ }
253
+ return false
254
+ }
255
+
256
+ func addMethodsToReachable(n *types.Named, pkgByTypes map[*types.Package]*packages.Package, rootDir string, reachable map[string]bool) {
257
+ pkg := pkgByTypes[n.Obj().Pkg()]
258
+ if pkg == nil || pkg.Fset == nil {
259
+ return
260
+ }
261
+ for i := 0; i < n.NumMethods(); i++ {
262
+ m := n.Method(i)
263
+ rel := relFile(pkg.Fset, m.Pos(), rootDir)
264
+ if rel == "" {
265
+ continue
266
+ }
267
+ name := n.Obj().Name() + "." + m.Name()
268
+ reachable[symbols.SymbolID(rel, name, "method")] = true
269
+ }
270
+ }
271
+
272
+ // ssaFuncSymbolID maps an *ssa.Function back to the Ctxo symbol id format
273
+ // used by the symbols/edges passes. Anonymous closures and synthetic
274
+ // wrappers are filtered by their missing source position.
275
+ func ssaFuncSymbolID(fn *ssa.Function, rootDir string) (string, bool) {
276
+ if fn == nil || fn.Pos() == token.NoPos || fn.Prog == nil {
277
+ return "", false
278
+ }
279
+ file := fn.Prog.Fset.File(fn.Pos())
280
+ if file == nil {
281
+ return "", false
282
+ }
283
+ rel, err := filepath.Rel(rootDir, file.Name())
284
+ if err != nil || strings.HasPrefix(rel, "..") {
285
+ return "", false
286
+ }
287
+ rel = filepath.ToSlash(rel)
288
+
289
+ name := fn.Name()
290
+ kind := "function"
291
+ if fn.Signature != nil && fn.Signature.Recv() != nil {
292
+ if recv := receiverName(fn.Signature.Recv().Type()); recv != "" {
293
+ name = recv + "." + fn.Name()
294
+ kind = "method"
295
+ }
296
+ }
297
+ return symbols.SymbolID(rel, name, kind), true
298
+ }
299
+
300
+ func receiverName(t types.Type) string {
301
+ if p, ok := t.(*types.Pointer); ok {
302
+ t = p.Elem()
303
+ }
304
+ if n, ok := t.(*types.Named); ok {
305
+ return n.Obj().Name()
306
+ }
307
+ return ""
308
+ }
309
+
310
+ func inModule(p *packages.Package, rootDir string) bool {
311
+ if len(p.GoFiles) == 0 {
312
+ return false
313
+ }
314
+ rel, err := filepath.Rel(rootDir, p.GoFiles[0])
315
+ return err == nil && !strings.HasPrefix(rel, "..")
316
+ }
317
+
318
+ func isExported(name string) bool {
319
+ return name != "" && name[0] >= 'A' && name[0] <= 'Z'
320
+ }
321
+
322
+ func relFile(fset *token.FileSet, pos token.Pos, rootDir string) string {
323
+ p := fset.Position(pos)
324
+ if !p.IsValid() {
325
+ return ""
326
+ }
327
+ rel, err := filepath.Rel(rootDir, p.Filename)
328
+ if err != nil || strings.HasPrefix(rel, "..") {
329
+ return ""
330
+ }
331
+ return filepath.ToSlash(rel)
332
+ }
@@ -0,0 +1,142 @@
1
+ package reach
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ "time"
8
+
9
+ "github.com/alperhankendi/ctxo/tools/ctxo-go-analyzer/internal/load"
10
+ )
11
+
12
+ func TestBinaryReachability(t *testing.T) {
13
+ dir := writeFixture(t, map[string]string{
14
+ "go.mod": "module fixture\n\ngo 1.22\n",
15
+ "main.go": `package main
16
+
17
+ func used() string { return "alive" }
18
+ func dead() string { return "never called" }
19
+
20
+ func main() { _ = used() }
21
+ `,
22
+ })
23
+
24
+ r := analyze(t, dir)
25
+ if !r.HasMain {
26
+ t.Fatal("expected HasMain=true for binary fixture")
27
+ }
28
+ if !r.Reachable["main.go::main::function"] {
29
+ t.Error("main should be reachable")
30
+ }
31
+ if !r.Reachable["main.go::used::function"] {
32
+ t.Error("used() should be reachable")
33
+ }
34
+ if r.Reachable["main.go::dead::function"] {
35
+ t.Error("dead() must not be reachable")
36
+ }
37
+ }
38
+
39
+ func TestLibraryMode(t *testing.T) {
40
+ dir := writeFixture(t, map[string]string{
41
+ "go.mod": "module fixture\n\ngo 1.22\n",
42
+ "lib.go": `package fixture
43
+
44
+ func Public() int { return internal() }
45
+ func internal() int { return 1 }
46
+ func Orphan() int { return 2 } // exported but with no internal callers — still reachable as library root
47
+ func _unused() int { return 3 } // not exported, no callers
48
+
49
+ type Box struct{}
50
+ func (Box) Shake() {} // exported method — reachable
51
+ func (Box) hide() {} // unexported — not reachable unless called
52
+ `,
53
+ })
54
+
55
+ r := analyze(t, dir)
56
+ if r.HasMain {
57
+ t.Fatal("expected library mode")
58
+ }
59
+ if !r.Reachable["lib.go::Public::function"] {
60
+ t.Error("Public must be reachable (library root)")
61
+ }
62
+ if !r.Reachable["lib.go::internal::function"] {
63
+ t.Error("internal must be reachable (called by Public)")
64
+ }
65
+ if !r.Reachable["lib.go::Orphan::function"] {
66
+ t.Error("Orphan must be reachable (library root)")
67
+ }
68
+ if !r.Reachable["lib.go::Box.Shake::method"] {
69
+ t.Error("Box.Shake must be reachable (exported method root)")
70
+ }
71
+ }
72
+
73
+ func TestReflectSafe(t *testing.T) {
74
+ dir := writeFixture(t, map[string]string{
75
+ "go.mod": "module fixture\n\ngo 1.22\n",
76
+ "main.go": `package main
77
+
78
+ import "reflect"
79
+
80
+ type Target struct{}
81
+ func (Target) UsedViaReflect() {} // never called, but accessed via reflect
82
+ func (Target) AlsoReflective() {}
83
+
84
+ func main() { _ = reflect.TypeOf(Target{}) }
85
+ `,
86
+ })
87
+ r := analyze(t, dir)
88
+ if !r.Reachable["main.go::Target.UsedViaReflect::method"] {
89
+ t.Error("reflect.TypeOf(Target{}) should mark Target methods reachable")
90
+ }
91
+ if !r.Reachable["main.go::Target.AlsoReflective::method"] {
92
+ t.Error("reflect-accessed types promote all methods")
93
+ }
94
+ }
95
+
96
+ func TestJSONUnmarshalSafe(t *testing.T) {
97
+ dir := writeFixture(t, map[string]string{
98
+ "go.mod": "module fixture\n\ngo 1.22\n",
99
+ "main.go": `package main
100
+
101
+ import "encoding/json"
102
+
103
+ type Payload struct{ Name string }
104
+ func (Payload) String() string { return "p" }
105
+
106
+ func main() {
107
+ var p Payload
108
+ _ = json.Unmarshal([]byte("{}"), &p)
109
+ }
110
+ `,
111
+ })
112
+ r := analyze(t, dir)
113
+ if !r.Reachable["main.go::Payload.String::method"] {
114
+ t.Error("json.Unmarshal argument type methods must be reachable")
115
+ }
116
+ }
117
+
118
+ // ── helpers ──
119
+
120
+ func analyze(t *testing.T, dir string) *Result {
121
+ t.Helper()
122
+ res := load.Packages(dir)
123
+ if res.FatalError != nil {
124
+ t.Fatal(res.FatalError)
125
+ }
126
+ return Analyze(dir, res.Packages, 30*time.Second)
127
+ }
128
+
129
+ func writeFixture(t *testing.T, files map[string]string) string {
130
+ t.Helper()
131
+ dir := t.TempDir()
132
+ for name, body := range files {
133
+ path := filepath.Join(dir, name)
134
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
135
+ t.Fatal(err)
136
+ }
137
+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
138
+ t.Fatal(err)
139
+ }
140
+ }
141
+ return dir
142
+ }
@@ -0,0 +1,172 @@
1
+ // Package symbols walks a loaded *packages.Package and produces emit.Symbol
2
+ // records grouped by source file. Maps Go declarations onto the kinds defined
3
+ // by @ctxo/plugin-api: function | class | interface | method | variable | type.
4
+ package symbols
5
+
6
+ import (
7
+ "go/ast"
8
+ "go/token"
9
+ "path/filepath"
10
+ "sort"
11
+ "strings"
12
+
13
+ "golang.org/x/tools/go/packages"
14
+
15
+ "github.com/alperhankendi/ctxo/tools/ctxo-go-analyzer/internal/emit"
16
+ )
17
+
18
+ // FileSymbols groups extracted symbols by the source file that declared them.
19
+ // RelPath is project-root-relative with forward-slash separators so it composes
20
+ // cleanly with the Ctxo symbol-id format on every platform.
21
+ type FileSymbols struct {
22
+ RelPath string
23
+ Symbols []emit.Symbol
24
+ }
25
+
26
+ // Extract walks every parsed file in the package and emits one Symbol per
27
+ // top-level declaration. Unexported names flow through; only the blank
28
+ // identifier "_" is skipped.
29
+ func Extract(pkg *packages.Package, rootDir string) []FileSymbols {
30
+ if pkg == nil || pkg.Fset == nil {
31
+ return nil
32
+ }
33
+
34
+ byFile := make(map[string]*FileSymbols)
35
+ getOrCreate := func(rel string) *FileSymbols {
36
+ if fs, ok := byFile[rel]; ok {
37
+ return fs
38
+ }
39
+ fs := &FileSymbols{RelPath: rel}
40
+ byFile[rel] = fs
41
+ return fs
42
+ }
43
+
44
+ for _, file := range pkg.Syntax {
45
+ rel := relativeFile(pkg.Fset, file, rootDir)
46
+ if rel == "" {
47
+ continue
48
+ }
49
+ fs := getOrCreate(rel)
50
+
51
+ for _, decl := range file.Decls {
52
+ switch d := decl.(type) {
53
+ case *ast.FuncDecl:
54
+ if d.Name == nil || d.Name.Name == "_" {
55
+ continue
56
+ }
57
+ kind := "function"
58
+ name := d.Name.Name
59
+ if d.Recv != nil && len(d.Recv.List) > 0 {
60
+ recv := receiverTypeName(d.Recv.List[0].Type)
61
+ if recv != "" {
62
+ name = recv + "." + d.Name.Name
63
+ }
64
+ kind = "method"
65
+ }
66
+ fs.Symbols = append(fs.Symbols, makeSymbol(pkg.Fset, rel, name, kind, d.Pos(), d.End()))
67
+
68
+ case *ast.GenDecl:
69
+ switch d.Tok {
70
+ case token.TYPE:
71
+ for _, spec := range d.Specs {
72
+ ts, ok := spec.(*ast.TypeSpec)
73
+ if !ok || ts.Name == nil || ts.Name.Name == "_" {
74
+ continue
75
+ }
76
+ kind := typeSpecKind(ts.Type)
77
+ fs.Symbols = append(fs.Symbols, makeSymbol(pkg.Fset, rel, ts.Name.Name, kind, ts.Pos(), ts.End()))
78
+ }
79
+ case token.VAR, token.CONST:
80
+ for _, spec := range d.Specs {
81
+ vs, ok := spec.(*ast.ValueSpec)
82
+ if !ok {
83
+ continue
84
+ }
85
+ for _, n := range vs.Names {
86
+ if n.Name == "_" {
87
+ continue
88
+ }
89
+ fs.Symbols = append(fs.Symbols, makeSymbol(pkg.Fset, rel, n.Name, "variable", n.Pos(), n.End()))
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ out := make([]FileSymbols, 0, len(byFile))
98
+ for _, v := range byFile {
99
+ sort.SliceStable(v.Symbols, func(i, j int) bool {
100
+ return v.Symbols[i].StartLine < v.Symbols[j].StartLine
101
+ })
102
+ out = append(out, *v)
103
+ }
104
+ sort.Slice(out, func(i, j int) bool { return out[i].RelPath < out[j].RelPath })
105
+ return out
106
+ }
107
+
108
+ // SymbolID composes the deterministic Ctxo symbol-id format.
109
+ func SymbolID(relFile, name, kind string) string {
110
+ return relFile + "::" + name + "::" + kind
111
+ }
112
+
113
+ // receiverTypeName strips pointer indirection and generic instantiation from
114
+ // a method receiver, returning the bare type name. Returns empty when the
115
+ // expression cannot be reduced to a single identifier.
116
+ func receiverTypeName(expr ast.Expr) string {
117
+ for {
118
+ switch x := expr.(type) {
119
+ case *ast.StarExpr:
120
+ expr = x.X
121
+ case *ast.IndexExpr: // generic with one type param: List[T]
122
+ expr = x.X
123
+ case *ast.IndexListExpr: // generic with multiple type params: Map[K, V]
124
+ expr = x.X
125
+ case *ast.Ident:
126
+ return x.Name
127
+ default:
128
+ return ""
129
+ }
130
+ }
131
+ }
132
+
133
+ func typeSpecKind(expr ast.Expr) string {
134
+ switch expr.(type) {
135
+ case *ast.StructType:
136
+ return "class"
137
+ case *ast.InterfaceType:
138
+ return "interface"
139
+ default:
140
+ return "type"
141
+ }
142
+ }
143
+
144
+ func makeSymbol(fset *token.FileSet, rel, name, kind string, pos, end token.Pos) emit.Symbol {
145
+ startPos := fset.Position(pos)
146
+ endPos := fset.Position(end)
147
+ startOffset := startPos.Offset
148
+ endOffset := endPos.Offset
149
+ return emit.Symbol{
150
+ SymbolID: SymbolID(rel, name, kind),
151
+ Name: name,
152
+ Kind: kind,
153
+ StartLine: startPos.Line,
154
+ EndLine: endPos.Line,
155
+ StartOffset: &startOffset,
156
+ EndOffset: &endOffset,
157
+ }
158
+ }
159
+
160
+ func relativeFile(fset *token.FileSet, file *ast.File, rootDir string) string {
161
+ tf := fset.File(file.Pos())
162
+ if tf == nil {
163
+ return ""
164
+ }
165
+ abs := tf.Name()
166
+ rel, err := filepath.Rel(rootDir, abs)
167
+ if err != nil || strings.HasPrefix(rel, "..") {
168
+ // Outside the module root (likely a stdlib or external dep) — skip.
169
+ return ""
170
+ }
171
+ return filepath.ToSlash(rel)
172
+ }