@ctxo/lang-go 0.8.0-alpha.0 → 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.
- package/package.json +7 -3
- package/tools/ctxo-go-analyzer/internal/edges/edges_test.go +6 -6
- package/tools/ctxo-go-analyzer/internal/extends/extends_test.go +3 -3
- package/tools/ctxo-go-analyzer/internal/implements/implements_test.go +6 -6
- package/tools/ctxo-go-analyzer/internal/load/load.go +102 -7
- package/tools/ctxo-go-analyzer/internal/load/load_test.go +96 -0
- package/tools/ctxo-go-analyzer/internal/reach/reach_test.go +3 -3
- package/tools/ctxo-go-analyzer/internal/symbols/symbols_test.go +6 -6
- package/tools/ctxo-go-analyzer/main.go +6 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctxo/lang-go",
|
|
3
|
-
"version": "0.8.0
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Ctxo Go language plugin (ctxo-go-analyzer + tree-sitter, full tier)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"tree-sitter-go": "^0.23.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@ctxo/plugin-api": "^0.7.
|
|
40
|
+
"@ctxo/plugin-api": "^0.7.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^22.15.3",
|
|
44
44
|
"tsup": "^8.4.0",
|
|
45
45
|
"typescript": "^5.8.3",
|
|
46
46
|
"vitest": "^3.1.3",
|
|
47
|
-
"@ctxo/plugin-api": "0.7.
|
|
47
|
+
"@ctxo/plugin-api": "0.7.1"
|
|
48
48
|
},
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"url": "https://github.com/alperhankendi/Ctxo/issues"
|
|
56
56
|
},
|
|
57
57
|
"homepage": "https://github.com/alperhankendi/Ctxo#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public",
|
|
60
|
+
"provenance": true
|
|
61
|
+
},
|
|
58
62
|
"scripts": {
|
|
59
63
|
"build": "tsup",
|
|
60
64
|
"typecheck": "tsc --noEmit",
|
|
@@ -81,9 +81,9 @@ func main() { _ = ID[int](1) }
|
|
|
81
81
|
`,
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
res
|
|
85
|
-
if
|
|
86
|
-
t.Fatal(
|
|
84
|
+
res := load.Packages(dir)
|
|
85
|
+
if res.FatalError != nil {
|
|
86
|
+
t.Fatal(res.FatalError)
|
|
87
87
|
}
|
|
88
88
|
ext := NewExtractor(dir, res.Packages)
|
|
89
89
|
|
|
@@ -127,9 +127,9 @@ func main() { _ = strings.ToUpper("hi") }
|
|
|
127
127
|
|
|
128
128
|
func extractAllEdges(t *testing.T, dir string) map[string]bool {
|
|
129
129
|
t.Helper()
|
|
130
|
-
res
|
|
131
|
-
if
|
|
132
|
-
t.Fatal(
|
|
130
|
+
res := load.Packages(dir)
|
|
131
|
+
if res.FatalError != nil {
|
|
132
|
+
t.Fatal(res.FatalError)
|
|
133
133
|
}
|
|
134
134
|
ext := NewExtractor(dir, res.Packages)
|
|
135
135
|
out := map[string]bool{}
|
|
@@ -103,9 +103,9 @@ type Guarded struct {
|
|
|
103
103
|
|
|
104
104
|
func extractAll(t *testing.T, dir string) map[string]bool {
|
|
105
105
|
t.Helper()
|
|
106
|
-
res
|
|
107
|
-
if
|
|
108
|
-
t.Fatal(
|
|
106
|
+
res := load.Packages(dir)
|
|
107
|
+
if res.FatalError != nil {
|
|
108
|
+
t.Fatal(res.FatalError)
|
|
109
109
|
}
|
|
110
110
|
out := map[string]bool{}
|
|
111
111
|
for _, list := range Extract(dir, res.Packages) {
|
|
@@ -39,9 +39,9 @@ type LoudGreeter interface {
|
|
|
39
39
|
`,
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
res
|
|
43
|
-
if
|
|
44
|
-
t.Fatal(
|
|
42
|
+
res := load.Packages(dir)
|
|
43
|
+
if res.FatalError != nil {
|
|
44
|
+
t.Fatal(res.FatalError)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
out := Extract(dir, res.Packages)
|
|
@@ -93,9 +93,9 @@ func (i ID) String() string { return string(i) }
|
|
|
93
93
|
`,
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
res
|
|
97
|
-
if
|
|
98
|
-
t.Fatal(
|
|
96
|
+
res := load.Packages(dir)
|
|
97
|
+
if res.FatalError != nil {
|
|
98
|
+
t.Fatal(res.FatalError)
|
|
99
99
|
}
|
|
100
100
|
out := Extract(dir, res.Packages)
|
|
101
101
|
|
|
@@ -5,7 +5,10 @@ package load
|
|
|
5
5
|
|
|
6
6
|
import (
|
|
7
7
|
"fmt"
|
|
8
|
+
"io/fs"
|
|
8
9
|
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"strings"
|
|
9
12
|
|
|
10
13
|
"golang.org/x/tools/go/packages"
|
|
11
14
|
)
|
|
@@ -24,18 +27,39 @@ const Mode = packages.NeedName |
|
|
|
24
27
|
packages.NeedTypesSizes |
|
|
25
28
|
packages.NeedModule
|
|
26
29
|
|
|
30
|
+
// skippedDirs are filesystem entries we never descend into during subdir
|
|
31
|
+
// fallback. vendor and node_modules avoid double-loading deps; hidden
|
|
32
|
+
// directories tend to be tooling state, not source.
|
|
33
|
+
var skippedDirs = map[string]bool{
|
|
34
|
+
"vendor": true,
|
|
35
|
+
".git": true,
|
|
36
|
+
".ctxo": true,
|
|
37
|
+
"node_modules": true,
|
|
38
|
+
"testdata": true,
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
// LoadResult bundles the loaded package set with any non-fatal errors so the
|
|
28
42
|
// caller can decide whether to surface them as warnings.
|
|
29
43
|
type LoadResult struct {
|
|
30
44
|
Packages []*packages.Package
|
|
31
|
-
// Errors are package-level Errors collected from
|
|
32
|
-
//
|
|
45
|
+
// Errors are package-level Errors collected from a Visit traversal.
|
|
46
|
+
// Returned for visibility — the loader does not fail the run on these.
|
|
33
47
|
Errors []packages.Error
|
|
48
|
+
// FatalError is set when packages.Load returned a top-level error (e.g.,
|
|
49
|
+
// module-graph conflict). The caller may still get partial Packages.
|
|
50
|
+
FatalError error
|
|
51
|
+
// FallbackUsed is true when the root pattern failed and per-subdir
|
|
52
|
+
// recovery was used to gather whatever packages still loaded cleanly.
|
|
53
|
+
FallbackUsed bool
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
// Packages loads "./..." rooted at dir. Patterns can override the default
|
|
37
57
|
// for tests that want to scope analysis to a single sub-package.
|
|
38
|
-
|
|
58
|
+
//
|
|
59
|
+
// Failure mode: returns a LoadResult (possibly with empty Packages) and
|
|
60
|
+
// sets FatalError. The caller should surface the error but continue —
|
|
61
|
+
// degraded analysis beats no analysis when consumers have a flaky module.
|
|
62
|
+
func Packages(dir string, patterns ...string) *LoadResult {
|
|
39
63
|
if len(patterns) == 0 {
|
|
40
64
|
patterns = []string{"./..."}
|
|
41
65
|
}
|
|
@@ -54,14 +78,85 @@ func Packages(dir string, patterns ...string) (*LoadResult, error) {
|
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
pkgs, err := packages.Load(cfg, patterns...)
|
|
81
|
+
res := &LoadResult{Packages: pkgs}
|
|
57
82
|
if err != nil {
|
|
58
|
-
|
|
83
|
+
res.FatalError = fmt.Errorf("load: %w", err)
|
|
59
84
|
}
|
|
60
85
|
|
|
61
|
-
var errs []packages.Error
|
|
62
86
|
packages.Visit(pkgs, nil, func(p *packages.Package) {
|
|
63
|
-
|
|
87
|
+
res.Errors = append(res.Errors, p.Errors...)
|
|
64
88
|
})
|
|
89
|
+
return res
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// PackagesWithFallback tries to load the whole module first; if that yields
|
|
93
|
+
// zero packages (typical when the module graph has a fatal conflict), it
|
|
94
|
+
// walks top-level subdirectories and loads each independently. Subdirs that
|
|
95
|
+
// transitively avoid the conflicting imports still produce full type info.
|
|
96
|
+
func PackagesWithFallback(dir string) *LoadResult {
|
|
97
|
+
primary := Packages(dir)
|
|
98
|
+
if len(primary.Packages) > 0 {
|
|
99
|
+
return primary
|
|
100
|
+
}
|
|
65
101
|
|
|
66
|
-
|
|
102
|
+
subdirs := topLevelGoSubdirs(dir)
|
|
103
|
+
if len(subdirs) == 0 {
|
|
104
|
+
return primary
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
merged := &LoadResult{
|
|
108
|
+
FatalError: primary.FatalError,
|
|
109
|
+
FallbackUsed: true,
|
|
110
|
+
}
|
|
111
|
+
for _, sub := range subdirs {
|
|
112
|
+
pattern := "./" + filepath.ToSlash(sub) + "/..."
|
|
113
|
+
subRes := Packages(dir, pattern)
|
|
114
|
+
if len(subRes.Packages) == 0 {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
merged.Packages = append(merged.Packages, subRes.Packages...)
|
|
118
|
+
merged.Errors = append(merged.Errors, subRes.Errors...)
|
|
119
|
+
}
|
|
120
|
+
return merged
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// topLevelGoSubdirs returns immediate subdirectories of root that contain at
|
|
124
|
+
// least one .go file (recursively). Skips vendor, .git, hidden dirs, etc.
|
|
125
|
+
func topLevelGoSubdirs(root string) []string {
|
|
126
|
+
entries, err := os.ReadDir(root)
|
|
127
|
+
if err != nil {
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
var out []string
|
|
131
|
+
for _, e := range entries {
|
|
132
|
+
if !e.IsDir() {
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
name := e.Name()
|
|
136
|
+
if skippedDirs[name] || strings.HasPrefix(name, ".") {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if hasGoFile(filepath.Join(root, name)) {
|
|
140
|
+
out = append(out, name)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func hasGoFile(dir string) bool {
|
|
147
|
+
found := false
|
|
148
|
+
_ = filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {
|
|
149
|
+
if err != nil {
|
|
150
|
+
return nil
|
|
151
|
+
}
|
|
152
|
+
if d.IsDir() && skippedDirs[d.Name()] {
|
|
153
|
+
return fs.SkipDir
|
|
154
|
+
}
|
|
155
|
+
if !d.IsDir() && strings.HasSuffix(d.Name(), ".go") {
|
|
156
|
+
found = true
|
|
157
|
+
return fs.SkipAll
|
|
158
|
+
}
|
|
159
|
+
return nil
|
|
160
|
+
})
|
|
161
|
+
return found
|
|
67
162
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package load
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"sort"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"golang.org/x/tools/go/packages"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestPackagesOnHealthyModule(t *testing.T) {
|
|
13
|
+
dir := writeFixture(t, map[string]string{
|
|
14
|
+
"go.mod": "module fixture\n\ngo 1.22\n",
|
|
15
|
+
"a.go": "package fixture\n\nfunc Hello() string { return \"hi\" }\n",
|
|
16
|
+
})
|
|
17
|
+
res := Packages(dir)
|
|
18
|
+
if res.FatalError != nil {
|
|
19
|
+
t.Fatalf("FatalError: %v", res.FatalError)
|
|
20
|
+
}
|
|
21
|
+
if len(res.Packages) == 0 {
|
|
22
|
+
t.Fatal("expected at least one package")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func TestPackagesWithFallbackRecoversPartial(t *testing.T) {
|
|
27
|
+
// "broken/" imports a non-existent path so the module-wide load fails;
|
|
28
|
+
// "healthy/" is self-contained, so per-subdir loading recovers it.
|
|
29
|
+
dir := writeFixture(t, map[string]string{
|
|
30
|
+
"go.mod": "module fixture\n\ngo 1.22\n",
|
|
31
|
+
"broken/broken.go": "package broken\n\nimport _ \"definitely.nowhere/missing\"\n",
|
|
32
|
+
"healthy/healthy.go": "package healthy\n\nfunc Y() int { return 1 }\n",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
res := PackagesWithFallback(dir)
|
|
36
|
+
if len(res.Packages) == 0 {
|
|
37
|
+
t.Fatalf("expected fallback to recover at least one package; got 0 (FatalError=%v)", res.FatalError)
|
|
38
|
+
}
|
|
39
|
+
if !pkgListContainsName(res.Packages, "healthy") {
|
|
40
|
+
t.Errorf("expected 'healthy' package recovered via fallback; got %v", pkgNames(res.Packages))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestTopLevelGoSubdirsSkipsHidden(t *testing.T) {
|
|
45
|
+
dir := writeFixture(t, map[string]string{
|
|
46
|
+
"a/file.go": "package a",
|
|
47
|
+
".hidden/x.go": "package x",
|
|
48
|
+
"vendor/foo.go": "package foo",
|
|
49
|
+
"b/file.go": "package b",
|
|
50
|
+
})
|
|
51
|
+
got := topLevelGoSubdirs(dir)
|
|
52
|
+
sort.Strings(got)
|
|
53
|
+
want := []string{"a", "b"}
|
|
54
|
+
if len(got) != len(want) {
|
|
55
|
+
t.Fatalf("got %v, want %v", got, want)
|
|
56
|
+
}
|
|
57
|
+
for i := range got {
|
|
58
|
+
if got[i] != want[i] {
|
|
59
|
+
t.Fatalf("index %d: got %q want %q", i, got[i], want[i])
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── helpers ──
|
|
65
|
+
|
|
66
|
+
func writeFixture(t *testing.T, files map[string]string) string {
|
|
67
|
+
t.Helper()
|
|
68
|
+
dir := t.TempDir()
|
|
69
|
+
for name, body := range files {
|
|
70
|
+
path := filepath.Join(dir, name)
|
|
71
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
72
|
+
t.Fatal(err)
|
|
73
|
+
}
|
|
74
|
+
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
75
|
+
t.Fatal(err)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return dir
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func pkgNames(pkgs []*packages.Package) []string {
|
|
82
|
+
out := make([]string, 0, len(pkgs))
|
|
83
|
+
for _, p := range pkgs {
|
|
84
|
+
out = append(out, p.Name)
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func pkgListContainsName(pkgs []*packages.Package, name string) bool {
|
|
90
|
+
for _, p := range pkgs {
|
|
91
|
+
if p.Name == name {
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
@@ -119,9 +119,9 @@ func main() {
|
|
|
119
119
|
|
|
120
120
|
func analyze(t *testing.T, dir string) *Result {
|
|
121
121
|
t.Helper()
|
|
122
|
-
res
|
|
123
|
-
if
|
|
124
|
-
t.Fatal(
|
|
122
|
+
res := load.Packages(dir)
|
|
123
|
+
if res.FatalError != nil {
|
|
124
|
+
t.Fatal(res.FatalError)
|
|
125
125
|
}
|
|
126
126
|
return Analyze(dir, res.Packages, 30*time.Second)
|
|
127
127
|
}
|
|
@@ -47,9 +47,9 @@ func (u User) ID() string { return u.Name }
|
|
|
47
47
|
`,
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
res
|
|
51
|
-
if
|
|
52
|
-
t.Fatalf("Packages: %v",
|
|
50
|
+
res := load.Packages(dir)
|
|
51
|
+
if res.FatalError != nil {
|
|
52
|
+
t.Fatalf("Packages: %v", res.FatalError)
|
|
53
53
|
}
|
|
54
54
|
if len(res.Packages) == 0 {
|
|
55
55
|
t.Fatal("no packages loaded")
|
|
@@ -123,9 +123,9 @@ type Box[T any] struct{ v T }
|
|
|
123
123
|
func (b *Box[T]) Get() T { return b.v }
|
|
124
124
|
`,
|
|
125
125
|
})
|
|
126
|
-
res
|
|
127
|
-
if
|
|
128
|
-
t.Fatal(
|
|
126
|
+
res := load.Packages(dir)
|
|
127
|
+
if res.FatalError != nil {
|
|
128
|
+
t.Fatal(res.FatalError)
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
found := false
|
|
@@ -66,9 +66,12 @@ func run(root string, stdout *os.File) error {
|
|
|
66
66
|
return err
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
res
|
|
70
|
-
if
|
|
71
|
-
|
|
69
|
+
res := load.PackagesWithFallback(root)
|
|
70
|
+
if res.FatalError != nil {
|
|
71
|
+
_ = w.Progress(fmt.Sprintf("module load returned a fatal error (degrading): %v", res.FatalError))
|
|
72
|
+
}
|
|
73
|
+
if res.FallbackUsed {
|
|
74
|
+
_ = w.Progress(fmt.Sprintf("root pattern failed; per-subdir fallback recovered %d packages", len(res.Packages)))
|
|
72
75
|
}
|
|
73
76
|
if len(res.Errors) > 0 {
|
|
74
77
|
_ = w.Progress(fmt.Sprintf("load reported %d package-level errors (continuing)", len(res.Errors)))
|