internal/deps/manifest.go

c000478e0378e2282fcdc6af384481c608d4fed2
gitbay/internal/deps/manifest.go history · blame · raw

81 lines · 2457 bytes

 1package deps
 2
 3import (
 4	"regexp"
 5	"sort"
 6	"strings"
 7)
 8
 9// Ecosystems, in the order they are reported.
10const (
11	EcoGo    = "go"
12	EcoNPM   = "npm"
13	EcoCargo = "cargo"
14	EcoPyPI  = "pypi"
15)
16
17// Dep is one direct dependency read from a manifest.
18type Dep struct {
19	Ecosystem string
20	Name      string
21	Current   string
22}
23
24// ReadFile returns a file from the tree being scanned, or an error when it
25// is absent. Manifests are read from the repository root only; a monorepo
26// with manifests in subdirectories is not scanned.
27type ReadFile func(path string) ([]byte, error)
28
29// MaxDeps bounds the work one repository can create for a sweep.
30const MaxDeps = 300
31
32// Scan returns the direct dependencies of every ecosystem whose manifest is
33// present, capped at MaxDeps. Dependencies whose version cannot be pinned to
34// an exact release — a range, a git or path source, a workspace member — are
35// left out: there is nothing meaningful to compare them against.
36func Scan(read ReadFile) []Dep {
37	var out []Dep
38	for _, parse := range []func(ReadFile) []Dep{parseGoMod, parseNPM, parseCargo, parsePython} {
39		out = append(out, parse(read)...)
40	}
41	sort.Slice(out, func(i, j int) bool {
42		if out[i].Ecosystem != out[j].Ecosystem {
43			return out[i].Ecosystem < out[j].Ecosystem
44		}
45		return out[i].Name < out[j].Name
46	})
47	if len(out) > MaxDeps {
48		out = out[:MaxDeps]
49	}
50	return out
51}
52
53// exactVersion matches a release we can compare: dot-separated numbers with
54// an optional suffix, no range operators left in it. wildcard catches the
55// ranges that survive that shape — "1.x", "2.*".
56var (
57	exactVersion = regexp.MustCompile(`^[0-9]+(\.[0-9]+)*([.\-+_a-zA-Z0-9]*)$`)
58	wildcard     = regexp.MustCompile(`(^|\.)[xX*](\.|$)`)
59)
60
61// pin reduces a version requirement to the single release it names, or ""
62// when it names a set rather than a release. A caret or tilde range pins its
63// floor, which is the version actually recorded in the manifest; anything
64// with alternatives, wildcards, or a non-registry source is skipped.
65func pin(spec string) string {
66	s := strings.TrimSpace(spec)
67	if s == "" || strings.ContainsAny(s, "|,* ") {
68		return ""
69	}
70	for _, bad := range []string{"workspace:", "npm:", "file:", "link:", "git", "http", "://"} {
71		if strings.Contains(s, bad) {
72			return ""
73		}
74	}
75	s = strings.TrimLeft(s, "^~>=<")
76	s = strings.TrimPrefix(s, "v")
77	if s == "" || wildcard.MatchString(s) || !exactVersion.MatchString(s) {
78		return ""
79	}
80	return s
81}