diff --git a/docs/projectgraph.md b/docs/projectgraph.md index 6cb9852e..078744d5 100644 --- a/docs/projectgraph.md +++ b/docs/projectgraph.md @@ -63,6 +63,22 @@ and no source text past a short call text. files of dependency code, and the report of the run. `Result.Unit` returns the indexes and the files of one unit of dependency code. +A generated Go file of a unit, with a `// Code generated ... DO NOT EDIT.` +line or a name that starts with `tables`, can hit a limit of the index, +as a table of thousands of entries. Its index then comes from the file +with each top-level var value that holds only data blanked. Function bodies +and values with a call or a function literal stay, so the calls of a +generated file, as a `.pb.go` file, stay in the graph. The index keeps the +hash of the file. If the skeleton still hits a limit, the file stays +truncated, and reach counts it as budget. + +The index leaves out a Go file of a unit only when no Go build reads it by +default: a `_test.go` file, a file with `//go:build ignore`, and a file +whose constraint needs a tag that the go command never sets, as `purego`. +It keeps the files of every GOOS and GOARCH, so a reach answer holds for +each platform that a project builds for. `Result.Excluded` lists the files +that it leaves out. + When the context ends, `Run` stops, marks each file that it did not index as `missing` and returns the result so far with the error of the context. The link step takes the partial result, and the reach query reports each @@ -147,8 +163,70 @@ of the highest confidence. A call that the link step cannot resolve is an unresolved site with a reason: `computed-name`, `dynamic-import`, `unknown-receiver`, -`no-definition`, `ambiguous` or `limit`. The site keeps the method name of -the call, so the reach query can tell which targets it concerns. +`no-definition`, `ambiguous`, `limit`, `interface-receiver`, +`trait-receiver` or `no-dependency-source`. The site keeps the method name of the call, so the +reach query can tell which targets it concerns. + +A call on the result of another call, as `html.NewTokenizer(r).Next()`, +needs the type of that result. When the link step does not know it, the +site is a record with the method name, so a target of that method is +`uncertain` and not `no-such-target`. + +### Result types + +The Go index records the type name of each result of a function, by +position, and each name that an assignment or a short variable declaration +binds to a result, as `z` in `z := html.NewTokenizer(r)` and `err` in +`n, err := html.Parse(r)`. The blank name `_` binds nothing. The link step +reads the definition of the function, in the project or in the unit of +dependency code that gives its package, and binds the type of the result +to the name. A call on the name then gets the external key of the method +of the type, as `golang.org//x//net//html//Tokenizer//Next`, and a package +edge joins the key to the definition in the unit. A chained call, as +`pkg.F().M()` or `a.B().C()`, takes the type of each result in turn. + +| Result | What the call gets | +| --- | --- | +| A type of a unit, or of the project | The edge to the method of the type | +| An interface type | The `cha` edges to the implementations in the graph. An implementation in a unit gets its edge through its external key. With no implementation, the site is a record with the reason `interface-receiver` | +| A function of a package that no unit gives | A record with the reason `no-dependency-source` | +| A result with no named type, or a function that the unit does not define | A record with the reason `unknown-receiver` | +| Two definitions of the function, as in two platform files of a Go package, with two result types | A record with the reason `unknown-receiver`. The walk follows one type, and a method of the other type would be `not-found` | + +Rust follows the same rules, with these additions. + +- The link step links the code of a `cargo:` unit, as the crate directory + in the cargo registry or in `vendor/`. The root module of the crate is + the unit, so two versions of one crate stay apart. The external key of + an item is the path that another crate writes, and an item of a private + module that the root gives on with `pub use` gets the shortest path, as + `smallvec//SmallVec`. +- A Cargo.toml can rename a dependency, as `helper = { package = "kit" }`. + A path of the crate writes `helper`, and the link step reads it as the + crate `kit` of the unit. The external node keeps the written name, as + `helper//serve`, and the package edge goes on to the definition in the + unit, whose external key is `kit//serve`. +- The index records the return type of each function and method, with + `Self` as the type of its `impl`. A type drops `&`, `&mut`, `Box<..>`, + `Rc<..>`, `Arc<..>` and its type arguments, so `&mut SmallVec<[u8; 4]>` + is `SmallVec`. An `Option` or a `Result` result keeps `T` in + `Returns` and its wrapper in `Wraps`. `unwrap` and `expect` give `T`, + and so does a `?`, which the index does not record. A call of another + method of the wrapper, as `map`, has no type. +- A declared type of a parameter, a field or a local that a unit gives + binds the receiver. An associated function, as `Type::from_vec(v)`, + binds the type that it returns, and `clone`, `to_owned`, `default` and + `from` on a type of a unit keep the type also when the crate derives + them. +- A trait object or a generic bound `T: Trait` of a unit gets the `cha` + edges to the implementations in the graph, through their external keys. + With no implementation, the site is a record with the reason + `trait-receiver`. + +The standard library is a unit too. Pass `GOROOT/src` as the unit +`go:std@`, as `-deps $(go env GOROOT)/src=go:std@go1.25.1`. It gives +each import path whose first element has no dot, as `net/http`. The index +step leaves out the `cmd` module of it, which no program imports. ## Overlay @@ -263,6 +341,14 @@ the key, so a query never holds a key that no node can have. | `reach.TargetByKey("yaml//load")` | The key, as a configuration or a flag writes it | `yaml//load` | | `reach.TargetByImport(langcode.Go, "os/exec", "Command")` | The module as the source of the language writes it, and the names under it | `os//exec//Command` | +A target also matches each function, method and constructor of a unit of +dependency code whose `Node.External` is the key, as the unexported +`go:golang.org/x/net@v0.61.0//html/render.go//render1` for +`golang.org//x//net//html//render1`. A call inside a unit goes to the +definition and not to the external key, so a target that only the code of +its unit calls is found through the definition. The call sites of a +target leave out the package edges, which join a key to its definition. + `reach.TargetsByKey` reads a list of keys. `Target.Key` returns the key, and the JSON form of a target is its key. One breadth-first search from the roots, and from the sources of the query, serves every target of a @@ -277,7 +363,7 @@ what the search did. | `found` | A path from a root reaches the target. `Path` holds the shortest one | | `not-found` | No path reaches the target, and no uncertainty concerns it | | `uncertain` | No path reaches the target, and an uncertainty concerns it | -| `no-such-target` | The key names no external node of the graph, and no uncertainty could name it | +| `no-such-target` | The key names no external node and no definition of a unit of the graph, and no uncertainty could name it | `reach.Result.String` writes a result as lines, and `reach.PathString` writes a path as `main.py:3 -> handlers.py:12 -> yaml//load`. diff --git a/examples/projectgraph/main.go b/examples/projectgraph/main.go index 1738c22d..97cfb662 100644 --- a/examples/projectgraph/main.go +++ b/examples/projectgraph/main.go @@ -13,8 +13,10 @@ // -import-dir venv/site-packages -deps venv/site-packages/yaml=pypi:PyYAML@6.0.1, // keeps its path under the import directory, as yaml/__init__.py. Any // other -deps directory is an import directory of its own, and a pypi -// package given that way is its package. With -summary it prints the -// summary of each such unit as JSON. +// package given that way is its package. The Go standard library is a unit +// too, as -deps $(go env GOROOT)/src=go:std@go1.25.1, and so is a Rust +// crate, as -deps vendor/smallvec=cargo:smallvec@1.13.2. With -summary it +// prints the summary of each such unit as JSON. package main import ( diff --git a/fileindex/index.go b/fileindex/index.go index e5ead0bc..53c8de84 100644 --- a/fileindex/index.go +++ b/fileindex/index.go @@ -19,7 +19,7 @@ import ( // Version is the version of the index records and of the rules that fill // them. Bump it when a field, a kind, an order or a fill rule changes. A // store keys an index by Version, so the bump makes every older index miss. -const Version = "11" +const Version = "14" // DefaultTestPaths are the globs that mark the test files of the common // layouts, as projectgraph.Options.TestPaths takes them. A glob with no "/" @@ -88,6 +88,16 @@ const ( UnresolvedNoDefinition UnresolvedReason = "no-definition" UnresolvedAmbiguous UnresolvedReason = "ambiguous" UnresolvedLimit UnresolvedReason = "limit" + // UnresolvedInterfaceReceiver is a call on a value of an interface + // type that no type of the graph implements, so its code is elsewhere. + UnresolvedInterfaceReceiver UnresolvedReason = "interface-receiver" + // UnresolvedTraitReceiver is a call on a Rust trait object or a value + // of a generic bound that no type of the graph implements. + UnresolvedTraitReceiver UnresolvedReason = "trait-receiver" + // UnresolvedNoDependencySource is a call on a value whose type comes + // from a package with no code in the graph, as the result of a + // function of a module that no unit gives. + UnresolvedNoDependencySource UnresolvedReason = "no-dependency-source" ) type FlagKind string @@ -204,8 +214,12 @@ type Export struct { // // Arity, Variadic and Returns hold the signature of a Go function or // method: the count of its parameters, a last parameter of the form ...T, -// and the type name of its first result. Static marks a static method of -// Java. +// and the type name of its first result. Results holds the type name of +// each result of a Go function with two or more results, by position, and +// "" for a result with no named type. Wraps names the Option or the Result +// that holds the result of a Rust function, and Returns then names the +// type inside, as Thing for -> Option. Static marks a static method +// of Java. type Definition struct { Key string `json:"key"` Kind DefKind `json:"kind"` @@ -221,6 +235,8 @@ type Definition struct { Arity int `json:"arity,omitempty"` Variadic bool `json:"variadic,omitempty"` Returns string `json:"returns,omitempty"` + Results []string `json:"results,omitempty"` + Wraps string `json:"wraps,omitempty"` Static bool `json:"static,omitempty"` Flags []Flag `json:"flags,omitempty"` } @@ -250,12 +266,14 @@ type Signature struct { // Typed is a parameter, a field or a local variable with the type that it // declares, as parser in run(parser: Parser). Type is the type name as // written, with no type arguments and no pointer, as Parser or ns.Parser. -// Result marks a name that holds the first result of a call. Type then -// names the function, as NewParser in p := NewParser(). +// Result marks a name that holds a result of a call. Type then names the +// function, as NewParser in p := NewParser(), and Index is the position of +// the result, as 1 for err in p, err := NewParser(). type Typed struct { Name string `json:"name"` Type string `json:"type"` Result bool `json:"result,omitempty"` + Index int `json:"index,omitempty"` } // Call is one call site. Caller is the definition key that holds the site. @@ -600,6 +618,8 @@ func (ix *Index) EachString(fn func(string) string) { typed(def.Params) typed(def.Locals) kind(&def.Returns) + strs(def.Results) + kind(&def.Wraps) flags(def.Flags) } for i := range ix.Classes { diff --git a/fileindex/testdata/index.json b/fileindex/testdata/index.json index a08c6158..bfd539ee 100644 --- a/fileindex/testdata/index.json +++ b/fileindex/testdata/index.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "src/app.py", "language": "python", "content_hash": "a172cedcae47474b615c54d510a5d84a8dea3032e958587430b413538be3f333", diff --git a/plugin/indexer/golang.go b/plugin/indexer/golang.go index c5d519ea..fcb30db1 100644 --- a/plugin/indexer/golang.go +++ b/plugin/indexer/golang.go @@ -184,7 +184,13 @@ func goSignature(p *projection, def *fileindex.Definition, node *sitter.Node) { var params []fileindex.Typed params, def.Arity, def.Variadic = p.goParams(node.ChildByFieldName("parameters"), p.goConstraints(node)) def.Params = append(def.Params, params...) - def.Returns = p.goResult(node.ChildByFieldName("result")) + results := p.goResults(node.ChildByFieldName("result")) + if len(results) > 0 { + def.Returns = results[0] + } + if len(results) > 1 { + def.Results = results + } def.Locals = p.goLocals(node.ChildByFieldName("body")) } @@ -253,20 +259,26 @@ func (p *projection) goParams(list *sitter.Node, constraints map[string]string) return typed, arity, variadic } -// goResult returns the type name of the first result of a function. -func (p *projection) goResult(result *sitter.Node) string { +// goResults returns the type name of each result of a function, by +// position. A result with no named type, as int or func(), gives "". +func (p *projection) goResults(result *sitter.Node) []string { if result == nil { - return "" + return nil } if result.Type() != "parameter_list" { - return p.goTypeName(result) + return []string{p.goTypeName(result)} } + var out []string for _, param := range ts.NamedChildren(result) { - if param.Type() == "parameter_declaration" { - return p.goTypeName(param.ChildByFieldName("type")) + if param.Type() != "parameter_declaration" { + continue + } + typ := p.goTypeName(param.ChildByFieldName("type")) + for range max(len(childrenByField(param, "name")), 1) { + out = append(out, typ) } } - return "" + return out } // goLocals returns the local variables of a function body whose type the @@ -297,21 +309,32 @@ func (p *projection) goLocals(body *sitter.Node) []fileindex.Typed { } // goAssigned returns the typed names of an assignment. One call on the -// right of more names gives its first result to the first name. +// right of more names gives each name the result at its position, and the +// blank name _ takes none. func (p *projection) goAssigned(names []*sitter.Node, right *sitter.Node) []fileindex.Typed { if right == nil { return nil } values := ts.NamedChildren(right) if len(values) == 1 && len(names) > 1 && values[0].Type() == "call_expression" { - names = names[:1] + t, ok := p.goValueType(values[0]) + if !ok || !t.Result { + return nil + } + var typed []fileindex.Typed + for i, name := range names { + if name.Type() == "identifier" && p.text(name) != "_" { + typed = append(typed, fileindex.Typed{Name: p.text(name), Type: t.Type, Result: true, Index: i}) + } + } + return typed } if len(values) != len(names) { return nil } var typed []fileindex.Typed for i, name := range names { - if name.Type() != "identifier" { + if name.Type() != "identifier" || p.text(name) == "_" { continue } if t, ok := p.goValueType(values[i]); ok { diff --git a/plugin/indexer/golang_test.go b/plugin/indexer/golang_test.go index 9927a568..0fcab12a 100644 --- a/plugin/indexer/golang_test.go +++ b/plugin/indexer/golang_test.go @@ -29,6 +29,7 @@ func TestFileIndexGo(t *testing.T) { " run(c.Add, k, f)", "}", "func Apply[T Saver, U any](t T, u U) { b := NewBox[int](1) }", + "func Pair() (_ *store.DB, ok, done bool) { _, x := New(\"y\"); return nil, x == nil, true }", }, "\n") + "\n" ix := indexSource(t, core.LanguageCodeGo, "shop.go", source) @@ -51,6 +52,7 @@ func TestFileIndexGo(t *testing.T) { assert.Equal(t, 2, def.Arity) assert.True(t, def.Variadic) assert.Equal(t, "Cart", def.Returns) + assert.Equal(t, []string{"Cart", ""}, def.Results, "a result of a predeclared type has no name") }) t.Run("a method has its receiver first and its typed locals", func(t *testing.T) { def := definitionOf(t, ix, "shop.go//Cart//Add") @@ -60,12 +62,19 @@ func TestFileIndexGo(t *testing.T) { assert.Equal(t, []fileindex.Typed{ {Name: "s", Type: "Saver"}, {Name: "d", Type: "New", Result: true}, + {Name: "err", Type: "New", Result: true, Index: 1}, {Name: "e", Type: "store.DB"}, {Name: "f", Type: "Cart"}, {Name: "g", Type: "Cart"}, {Name: "h", Type: "store.Open", Result: true}, }, def.Locals) }) + t.Run("each result has its position and the blank name binds none", func(t *testing.T) { + def := definitionOf(t, ix, "shop.go//Pair") + assert.Equal(t, "store.DB", def.Returns) + assert.Equal(t, []string{"store.DB", "", ""}, def.Results) + assert.Equal(t, []fileindex.Typed{{Name: "x", Type: "New", Result: true, Index: 1}}, def.Locals) + }) t.Run("a type parameter stands for its constraint", func(t *testing.T) { def := definitionOf(t, ix, "shop.go//Apply") assert.Equal(t, []fileindex.Typed{{Name: "t", Type: "Saver"}}, def.Params) diff --git a/plugin/indexer/gounit.go b/plugin/indexer/gounit.go new file mode 100644 index 00000000..421c8150 --- /dev/null +++ b/plugin/indexer/gounit.go @@ -0,0 +1,178 @@ +package indexer + +import ( + "bytes" + "context" + "go/build" + "io" + "path" + "regexp" + "slices" + "strings" + + "github.com/safedep/code/core" + "github.com/safedep/code/fileindex" + "github.com/safedep/code/langcode" + "github.com/safedep/code/pkg/ts" + sitter "github.com/smacker/go-tree-sitter" +) + +// goOS and goArch are the GOOS and GOARCH values of the Go toolchain. A +// reach answer must hold for every platform that a project builds for, so +// the index keeps a file of a dependency unit when the build of any pair +// reads it. An index of one platform drops the other twin of a +// thing_linux.go and thing_darwin.go pair, and a call that only the darwin +// build makes then reads as no-such-target on a darwin host. +var ( + goOS = []string{"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js", "linux", "netbsd", "openbsd", "plan9", "solaris", "wasip1", "windows", "zos"} + goArch = []string{"386", "amd64", "arm", "arm64", "loong64", "mips", "mipsle", "mips64", "mips64le", "ppc64", "ppc64le", "riscv64", "s390x", "sparc64", "wasm"} +) + +// goOutOfBuild reports whether the go command never builds a file of a Go +// dependency unit for an importer, by its path: a file that is not Go +// source, as the JavaScript of a web page in GOROOT/src, a test file, a +// file under a testdata directory or a directory whose name starts with _ +// or ., and a file of the cmd module of the unit of the standard library, +// as go:std@go1.25.1//cmd/go/main.go, which no program imports. Such a file +// holds no code that an importer runs, and its package clause can differ +// from the clause of its directory, as package main of a fixture. +func goOutOfBuild(file core.File) bool { + unit, inner := fileindex.SplitUnit(file.Name()) + pkg, err := fileindex.ParseUnit(unit) + if !file.IsImport() || err != nil || pkg.Ecosystem != "go" { + return false + } + if path.Ext(inner) != ".go" || strings.HasSuffix(inner, "_test.go") { + return true + } + dirs := strings.Split(path.Dir(inner), "/") + if pkg.Name == "std" && dirs[0] == "cmd" { + return true + } + return slices.ContainsFunc(dirs, func(d string) bool { + return d == "testdata" || strings.HasPrefix(d, "_") || strings.HasPrefix(d, ".") && d != "." + }) +} + +// goExcluded reports whether no Go build reads a Go file of a dependency +// unit by its build constraint: a file with //go:build ignore, or a file whose +// constraint needs a tag that the go command never sets by itself, as +// purego. Such a file holds no code that an importer runs by default. Its +// package clause can differ from the clause of its directory, as package +// main of a gen.go, and its index would hold calls that no program makes. +// A build constraint that the build cannot read leaves the file in, so the +// index loses no code. +func goExcluded(file core.File, data []byte) bool { + if !file.IsImport() || path.Ext(file.Name()) != ".go" { + return false + } + name := path.Base(file.Name()) + ctxt := build.Default + ctxt.CgoEnabled = true + ctxt.BuildTags, ctxt.ToolTags = nil, nil + ctxt.OpenFile = func(string) (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(data)), nil } + for _, os := range goOS { + for _, arch := range goArch { + ctxt.GOOS, ctxt.GOARCH = os, arch + if match, err := ctxt.MatchFile(".", name); err != nil || match { + return false + } + } + } + return true +} + +// generatedHeader matches the comment line that marks a generated Go file: +// the Code generated ... DO NOT EDIT. line of the Go convention, or an older +// line as generated by go run gen.go; DO NOT EDIT of x/net. +var generatedHeader = regexp.MustCompile(`(?m)^//.*\bDO NOT EDIT\b`) + +// packageClause matches the package clause of a Go file. The marker of a +// generated file comes before it. +var packageClause = regexp.MustCompile(`(?m)^package `) + +// isGeneratedTable reports whether a Go file of a dependency unit is a +// generated file or a table file, as idna/tables15.0.0.go or +// publicsuffix/table.go of x/net. +func isGeneratedTable(name string, data []byte) bool { + if unit, _ := fileindex.SplitUnit(name); unit == "" || path.Ext(name) != ".go" { + return false + } + if base := path.Base(name); strings.HasPrefix(base, "table") { + return true + } + head := data + if loc := packageClause.FindIndex(data); loc != nil { + head = data[:loc[0]] + } + return generatedHeader.Match(head) +} + +// goSkeleton returns the bytes of a Go file with each value of a +// top-level var or const that holds only data replaced by 0 and spaces. The +// declarations keep their lines and columns, and the file still parses. The +// data values of a generated table file are what hit the limits of the +// index, as a string of thousands of concatenated parts. Function bodies and +// any value with a call or a function literal stay, because a generated +// file can hold code that a program runs, as a .pb.go file. +func goSkeleton(tree *sitter.Tree, data []byte) []byte { + out := bytes.Clone(data) + clear := func(from, to uint32) { + for i := from; i < to; i++ { + if out[i] != '\n' { + out[i] = ' ' + } + } + } + var visit func(node *sitter.Node) + visit = func(node *sitter.Node) { + switch node.Type() { + case "function_declaration", "method_declaration": + return + case "var_spec", "const_spec": + if values := node.ChildByFieldName("value"); values != nil { + for _, v := range ts.NamedChildren(values) { + if !holdsCode(v) { + clear(v.StartByte(), v.EndByte()) + out[v.StartByte()] = '0' + } + } + } + return + } + for _, child := range ts.NamedChildren(node) { + visit(child) + } + } + visit(tree.RootNode()) + return out +} + +// holdsCode reports whether an expression holds a call or a function +// literal, which a program runs when it initializes the package. +func holdsCode(node *sitter.Node) bool { + if t := node.Type(); t == "call_expression" || t == "func_literal" { + return true + } + return slices.ContainsFunc(ts.NamedChildren(node), holdsCode) +} + +// skeletonIndex indexes the skeleton of a generated table file of a +// dependency unit whose index hit a limit. The index keeps the hash of the +// bytes of the file, so the store serves it on the next run. +func skeletonIndex(ctx context.Context, p contentParser, file pending, tree core.ParseTree, ix *fileindex.Index) (*fileindex.Index, error) { + if ix == nil || ix.Language != langcode.Go || len(ix.Truncated) == 0 || partial(ix) || !isGeneratedTable(file.file.Name(), file.data) { + return ix, nil + } + skeleton, err := p.ParseContent(ctx, file.file, goSkeleton(tree.Tree(), file.data)) + if err != nil { + return nil, err + } + defer skeleton.Close() + out, err := project(ctx, skeleton) + if err != nil { + return nil, err + } + out.ContentHash = file.key.ContentHash + return out, nil +} diff --git a/plugin/indexer/gounit_test.go b/plugin/indexer/gounit_test.go new file mode 100644 index 00000000..24551c7e --- /dev/null +++ b/plugin/indexer/gounit_test.go @@ -0,0 +1,117 @@ +package indexer + +import ( + "context" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/safedep/code/core" + "github.com/safedep/code/fileindex" + "github.com/safedep/code/fileindex/store/memstore" + "github.com/safedep/code/fs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const goKitUnit = "go:example.com/kit@v1.0.0" + +// goKitFiles is a Go module with a file of each kind that the index keeps +// or leaves out. tables.go is a generated table file whose var holds a +// string of 60 parts, which nests past the depth limit of the index. Its +// Lookup calls helper, and its second var calls helper too. +func goKitFiles() map[string]string { + parts := strings.Repeat(` + "x"`, 60) + return map[string]string{ + "go.mod": "module example.com/kit\n\ngo 1.22\n", + "kit.go": "package kit\n\nfunc Run() { helper() }\n\nfunc helper() {}\n", + "kit_test.go": "package kit\n\nimport \"testing\"\n\nfunc TestRun(t *testing.T) { Run() }\n", + "kit_windows.go": "//go:build windows\n\npackage kit\n\nfunc OnlyWindows() {}\n", + "kit_linux.go": "package kit\n\nfunc OnlyLinux() {}\n", + "kit_purego.go": "//go:build purego\n\npackage kit\n\nfunc PureGo() {}\n", + "gen.go": "//go:build ignore\n\npackage main\n\nfunc main() {}\n", + "tables.go": "// Code generated by synth. DO NOT EDIT.\n\npackage kit\n\nfunc Lookup() int { helper(); return len(data) }\n\nvar data string = \"\"" + parts + "\n\nvar ready = helper\n", + } +} + +func TestIndexerReadsTheGoBuildsOfADependencyUnit(t *testing.T) { + dir := t.TempDir() + writeFiles(t, dir, map[string]string{"main.go": "package main\n\nfunc main() {}\n", "main_test.go": "package main\n"}) + kit := filepath.Join(dir, "vendor", "kit") + writeFiles(t, kit, goKitFiles()) + fsys, err := fs.NewLocalFileSystem(fs.LocalFileSystemConfig{ + AppDirectories: []string{dir}, + ImportDirectories: []string{kit}, + ImportUnits: map[string]string{kit: goKitUnit}, + ExcludePatterns: []*regexp.Regexp{fs.SkipPattern(kit)}, + }) + require.NoError(t, err) + result, err := runIndexer(t, context.Background(), fsys, Options{Languages: languages(t, core.LanguageCodeGo), Store: memstore.New(), Imports: true}) + require.NoError(t, err) + + var indexed, excluded []string + for p, status := range result.Dependencies { + _, inner := fileindex.SplitUnit(p) + assert.Equal(t, fileindex.FileIndexed, status, p) + indexed = append(indexed, inner) + } + for _, p := range result.Excluded { + _, inner := fileindex.SplitUnit(p) + excluded = append(excluded, inner) + } + assert.ElementsMatch(t, []string{"kit.go", "kit_linux.go", "kit_windows.go", "tables.go"}, indexed, "a file that any platform builds stays") + assert.ElementsMatch(t, []string{"gen.go", "kit_purego.go", "kit_test.go"}, excluded) + assert.Equal(t, map[string]fileindex.FileStatus{"main.go": fileindex.FileIndexed, "main_test.go": fileindex.FileIndexed}, result.Files, "a test file of the project stays") + + var tables *fileindex.Index + for _, ix := range result.Indexes { + if ix.Path == goKitUnit+"//tables.go" { + tables = ix + } + } + require.NotNil(t, tables) + assert.Empty(t, tables.Truncated, "the skeleton of the table file hits no limit") + var keys, callees []string + for _, def := range tables.Definitions { + keys = append(keys, def.Key) + } + for _, call := range tables.Calls { + callees = append(callees, call.Callee) + } + assert.Contains(t, keys, goKitUnit+"//tables.go//Lookup") + assert.Contains(t, callees, "helper", "the skeleton keeps the bodies") +} + +func TestGoSkeletonKeepsTheHashOfTheFile(t *testing.T) { + dir := t.TempDir() + kit := filepath.Join(dir, "kit") + writeFiles(t, kit, goKitFiles()) + fsys, err := fs.NewLocalFileSystem(fs.LocalFileSystemConfig{AppDirectories: []string{dir}, ImportDirectories: []string{kit}, ImportUnits: map[string]string{kit: goKitUnit}, ExcludePatterns: []*regexp.Regexp{fs.SkipPattern(kit)}}) + require.NoError(t, err) + s := memstore.New() + opts := Options{Languages: languages(t, core.LanguageCodeGo), Store: s, Imports: true} + _, err = runIndexer(t, context.Background(), fsys, opts) + require.NoError(t, err) + result, err := runIndexer(t, context.Background(), fsys, opts) + require.NoError(t, err) + assert.Equal(t, fileindex.FileCached, result.Dependencies[goKitUnit+"//tables.go"], "the store serves the skeleton index on the next run") +} + +func TestIsGeneratedTable(t *testing.T) { + for _, tt := range []struct { + name string + data string + want bool + }{ + {name: goKitUnit + "//gen/out.go", data: "// Code generated by synth. DO NOT EDIT.\n\npackage gen\n", want: true}, + {name: goKitUnit + "//publicsuffix/list.go", data: "// generated by go run gen.go; DO NOT EDIT\n\npackage publicsuffix\n", want: true}, + {name: goKitUnit + "//idna/tables15.0.0.go", data: "package idna\n", want: true}, + {name: goKitUnit + "//kit.go", data: "package kit\n\n// DO NOT EDIT after the package clause.\n", want: false}, + {name: "tables.go", data: "// Code generated by synth. DO NOT EDIT.\n\npackage main\n", want: false}, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, isGeneratedTable(tt.name, []byte(tt.data))) + }) + } +} diff --git a/plugin/indexer/indexer.go b/plugin/indexer/indexer.go index 562acffd..2c8e4a7e 100644 --- a/plugin/indexer/indexer.go +++ b/plugin/indexer/indexer.go @@ -79,6 +79,12 @@ type Result struct { // Report lists the files that the run parsed, skipped and failed. Report *plugin.Report + + // Excluded lists the Go files of dependency code that no Go build + // reads by default, in walk order: test files, files with //go:build + // ignore and files that need a tag the go command never sets. They have + // no status, as no program runs their code. + Excluded []string } // Statuses returns the status of each file of the run, of the project and @@ -166,6 +172,8 @@ type run struct { result *Result pool *pool strs interner + // excluded holds the names of Result.Excluded. + excluded map[string]bool } // Run indexes the app files of fsys, and its import files with @@ -190,7 +198,8 @@ func (ix *Indexer) Run(ctx context.Context, fsys core.ImportAwareFileSystem) (*R } r := &run{ - opts: ix.opts, + opts: ix.opts, + excluded: map[string]bool{}, result: &Result{ Files: make(map[string]fileindex.FileStatus, len(files)), Dependencies: map[string]fileindex.FileStatus{}, @@ -206,7 +215,7 @@ func (ix *Indexer) Run(ctx context.Context, fsys core.ImportAwareFileSystem) (*R for _, file := range files { statuses := r.result.statuses(file.Name()) _, done := statuses[file.Name()] - if !done && !slices.Contains(r.result.Report.Skipped, file.Name()) { + if !done && !r.excluded[file.Name()] && !slices.Contains(r.result.Report.Skipped, file.Name()) { statuses[file.Name()] = fileindex.FileMissing } } @@ -260,11 +269,19 @@ func (r *run) index(ctx context.Context, files []core.File, byExtension map[stri r.result.Report.Skipped = append(r.result.Report.Skipped, file.Name()) continue } + if goOutOfBuild(file) { + r.exclude(file) + continue + } data, err := read(file, r.opts.MaxFileBytes) if err != nil { r.fail(ctx, file, core.StageRead, err) continue } + if goExcluded(file, data) { + r.exclude(file) + continue + } batch = append(batch, pending{ file: file, @@ -448,6 +465,13 @@ func (r *run) done(file core.File, ix *fileindex.Index, status fileindex.FileSta r.result.Indexes = append(r.result.Indexes, ix) } +// exclude records a file of dependency code that the build of the platform +// does not read. +func (r *run) exclude(file core.File) { + r.excluded[file.Name()] = true + r.result.Excluded = append(r.result.Excluded, file.Name()) +} + func (r *run) fail(ctx context.Context, file core.File, stage core.Stage, err error) { r.result.Report.Failed = append(r.result.Report.Failed, plugin.FileError{File: file.Name(), Stage: stage, Err: err}) r.result.statuses(file.Name())[file.Name()] = status(ctx) diff --git a/plugin/indexer/indexer_pool.go b/plugin/indexer/indexer_pool.go index f0982de6..40797ffb 100644 --- a/plugin/indexer/indexer_pool.go +++ b/plugin/indexer/indexer_pool.go @@ -122,5 +122,8 @@ func analyze(ctx context.Context, p contentParser, file pending) outcome { return outcome{tree: tree} } err = plugin.Analyze(ctx, indexPlugin, file.file, tree) + if err == nil { + index, err = skeletonIndex(ctx, p, file, tree, index) + } return outcome{tree: tree, index: index, indexErr: err, stage: core.Stage(indexPlugin.Name())} } diff --git a/plugin/indexer/plugin_test.go b/plugin/indexer/plugin_test.go index b920e4c6..175c5883 100644 --- a/plugin/indexer/plugin_test.go +++ b/plugin/indexer/plugin_test.go @@ -100,6 +100,9 @@ func TestFileIndexGolden(t *testing.T) { var goldenHashes = map[string]string{ "10": "6053fe7c2b8fef2888806debc47372948eec366beb800419c1e435f021711fa0", "11": "141e6d6a6f6d369635bf4f6781033acc269e59fe5f722558192b8e952c7e7bd9", + "12": "974943df4185f9c3070dcfdfedff8e91912e257d255a936f7c27608bb63a69f0", + "13": "97614d8b96782e65813ee409e4a3f33c4655157b5dfa58baf17cd3677958c619", + "14": "26f22c80fd4120cd04f228a53188accb1695434d0ed1ff5654b110981f8f44dd", } func TestFileIndexGoldenHashFollowsVersion(t *testing.T) { diff --git a/plugin/indexer/rust.go b/plugin/indexer/rust.go index cfa2e598..610908d9 100644 --- a/plugin/indexer/rust.go +++ b/plugin/indexer/rust.go @@ -194,6 +194,22 @@ func (p *projection) rustTypeName(node *sitter.Node, bounds map[string]string) s return "" } +// rustWrappers are the types of std whose value ?, unwrap and expect give, +// as T of Option and of Result. +var rustWrappers = []string{"Option", "Result"} + +// rustResult returns the type that a result names, and the Option or the +// Result that holds it, as Thing and Option for Option>. +func (p *projection) rustResult(node *sitter.Node, bounds map[string]string) (string, string) { + if node != nil && node.Type() == "generic_type" { + name := p.rustTypeName(node.ChildByFieldName("type"), bounds) + if args := ts.NamedChildren(node.ChildByFieldName("type_arguments")); slices.Contains(rustWrappers, name) && len(args) > 0 { + return p.rustTypeName(args[0], bounds), name + } + } + return p.rustTypeName(node, bounds), "" +} + // rustBounds returns the first trait bound of each type parameter of a // function and of its where clause. func (p *projection) rustBounds(fn *sitter.Node) map[string]string { @@ -259,7 +275,7 @@ func rustSignature(p *projection, def *fileindex.Definition, node *sitter.Node) def.Params = append(def.Params, fileindex.Typed{Name: p.text(name), Type: typ}) } } - def.Returns = p.rustTypeName(node.ChildByFieldName("return_type"), bounds) + def.Returns, def.Wraps = p.rustResult(node.ChildByFieldName("return_type"), bounds) def.Locals = p.rustLocals(node.ChildByFieldName("body"), bounds) if impl := rustImpl(node); impl != nil && !def.Static && !p.rustDefinesType(impl) { def.Params = append([]fileindex.Typed{{Name: extensionParam, Type: p.rustItemName(impl)}}, def.Params...) diff --git a/plugin/indexer/testdata/golden/App.cs.json b/plugin/indexer/testdata/golden/App.cs.json index 3d97a199..7a21f6ce 100644 --- a/plugin/indexer/testdata/golden/App.cs.json +++ b/plugin/indexer/testdata/golden/App.cs.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "App.cs", "language": "csharp", "content_hash": "8991919e54610418b58a53fb6b26dc563c5ef5af684165b60e8af5b6e7e8773c", diff --git a/plugin/indexer/testdata/golden/App.java.json b/plugin/indexer/testdata/golden/App.java.json index e18ad31c..0ea48aa0 100644 --- a/plugin/indexer/testdata/golden/App.java.json +++ b/plugin/indexer/testdata/golden/App.java.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "App.java", "language": "java", "content_hash": "4b93f5c2c4ed8e1881cb857a9b7cd65c3a0d4cb4cd0929140539d0a418a27a95", diff --git a/plugin/indexer/testdata/golden/App.kt.json b/plugin/indexer/testdata/golden/App.kt.json index a82b34c4..220cf808 100644 --- a/plugin/indexer/testdata/golden/App.kt.json +++ b/plugin/indexer/testdata/golden/App.kt.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "App.kt", "language": "kotlin", "content_hash": "458147a6c6ebc2561187207bb3bca7e091914bfd2e52f0540f58887976983462", diff --git a/plugin/indexer/testdata/golden/app.go.json b/plugin/indexer/testdata/golden/app.go.json index 1de97921..1ed55cf9 100644 --- a/plugin/indexer/testdata/golden/app.go.json +++ b/plugin/indexer/testdata/golden/app.go.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.go", "language": "go", "content_hash": "3d65f36ed0edfd5e009c06d07d3d038337e9faebaca6c9762b5c473a14826166", @@ -86,7 +86,11 @@ "exported": true, "arity": 2, "variadic": true, - "returns": "Server" + "returns": "Server", + "results": [ + "Server", + "" + ] }, { "key": "app.go//Server//Run", @@ -125,6 +129,12 @@ "type": "NewServer", "result": true }, + { + "name": "err", + "type": "NewServer", + "result": true, + "index": 1 + }, { "name": "e", "type": "store.DB" diff --git a/plugin/indexer/testdata/golden/app.js.json b/plugin/indexer/testdata/golden/app.js.json index 8257cb5a..d35c1491 100644 --- a/plugin/indexer/testdata/golden/app.js.json +++ b/plugin/indexer/testdata/golden/app.js.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.js", "language": "javascript", "content_hash": "247fdd512217fb5e7ce19cbf3f60b327e145abc15a26ee510e167cbc651b0a54", diff --git a/plugin/indexer/testdata/golden/app.php.json b/plugin/indexer/testdata/golden/app.php.json index f7b828de..22c23723 100644 --- a/plugin/indexer/testdata/golden/app.php.json +++ b/plugin/indexer/testdata/golden/app.php.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.php", "language": "php", "content_hash": "b57d8d52aafac8d7cbe2d19e5889e4e7b851ee71a48ea966cdd9cbfd14214466", diff --git a/plugin/indexer/testdata/golden/app.py.json b/plugin/indexer/testdata/golden/app.py.json index 97bb677c..67e1a583 100644 --- a/plugin/indexer/testdata/golden/app.py.json +++ b/plugin/indexer/testdata/golden/app.py.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.py", "language": "python", "content_hash": "a9cdb5fb012d2a29970c07deda9cc8360983015e2360cb9091bca7bfce5c801d", diff --git a/plugin/indexer/testdata/golden/app.rb.json b/plugin/indexer/testdata/golden/app.rb.json index 2b3b13f9..ed1ac686 100644 --- a/plugin/indexer/testdata/golden/app.rb.json +++ b/plugin/indexer/testdata/golden/app.rb.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.rb", "language": "ruby", "content_hash": "f3a3e601660e0130750119df4267cac37972c412ccc8f5d8f16e3ba516f659d9", diff --git a/plugin/indexer/testdata/golden/app.rs.json b/plugin/indexer/testdata/golden/app.rs.json index 92011f3a..a57077f2 100644 --- a/plugin/indexer/testdata/golden/app.rs.json +++ b/plugin/indexer/testdata/golden/app.rs.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.rs", "language": "rust", "content_hash": "f07a751f9610f47e7b49679223d4ca80f85ecb21abde61488935d2f21a5c3603", @@ -76,7 +76,7 @@ "type": "String" } ], - "returns": "Result" + "wraps": "Result" }, { "key": "app.rs//App//audit", diff --git a/plugin/indexer/testdata/golden/app.ts.json b/plugin/indexer/testdata/golden/app.ts.json index d61ed00c..9e409080 100644 --- a/plugin/indexer/testdata/golden/app.ts.json +++ b/plugin/indexer/testdata/golden/app.ts.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "app.ts", "language": "typescript", "content_hash": "d3908948ab8ec7b3adc898e0707ce74bb3a2d6393a724dfbeb67147840397234", diff --git a/plugin/indexer/testdata/golden/exports.js.json b/plugin/indexer/testdata/golden/exports.js.json index 0d68280d..5e68f690 100644 --- a/plugin/indexer/testdata/golden/exports.js.json +++ b/plugin/indexer/testdata/golden/exports.js.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "exports.js", "language": "javascript", "content_hash": "765716b5977b05d7bbc4ef5e0192e196e0209400366036053df632d54fdb0f95", diff --git a/plugin/indexer/testdata/golden/exports.py.json b/plugin/indexer/testdata/golden/exports.py.json index 3287b4ba..0acadd05 100644 --- a/plugin/indexer/testdata/golden/exports.py.json +++ b/plugin/indexer/testdata/golden/exports.py.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "exports.py", "language": "python", "content_hash": "653115ddc22536763049e9209d4942bca5c6817b98bc01722879b69c04d8605b", diff --git a/plugin/indexer/testdata/golden/exports.ts.json b/plugin/indexer/testdata/golden/exports.ts.json index be4306a3..78e7f72a 100644 --- a/plugin/indexer/testdata/golden/exports.ts.json +++ b/plugin/indexer/testdata/golden/exports.ts.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "exports.ts", "language": "typescript", "content_hash": "0edcb9324080b924f3ac45d57f5b5cbe90d625038a0dd7f99fa9e5fcbeee1edc", diff --git a/plugin/indexer/testdata/golden/flags.js.json b/plugin/indexer/testdata/golden/flags.js.json index 1fa276f7..78abeb49 100644 --- a/plugin/indexer/testdata/golden/flags.js.json +++ b/plugin/indexer/testdata/golden/flags.js.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "flags.js", "language": "javascript", "content_hash": "70731d0448bc5af9ecf5c81156eefbd39535a3fc674fffc250937d76c2ba4610", diff --git a/plugin/indexer/testdata/golden/flags.py.json b/plugin/indexer/testdata/golden/flags.py.json index d4a3382c..3175a29a 100644 --- a/plugin/indexer/testdata/golden/flags.py.json +++ b/plugin/indexer/testdata/golden/flags.py.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "flags.py", "language": "python", "content_hash": "d3169e0c3c00508c3f9492866cf4db99ddd780d42dc9e4438558d1d4a22f2621", diff --git a/plugin/indexer/testdata/golden/flags.ts.json b/plugin/indexer/testdata/golden/flags.ts.json index a0741c03..74721a1c 100644 --- a/plugin/indexer/testdata/golden/flags.ts.json +++ b/plugin/indexer/testdata/golden/flags.ts.json @@ -1,5 +1,5 @@ { - "version": "11", + "version": "14", "path": "flags.ts", "language": "typescript", "content_hash": "a84c4ad21735c1c250e33317463237749489ea1f220a93f97e2bea3f07b040c4", diff --git a/projectgraph/cha.go b/projectgraph/cha.go index 376c4340..c26ff585 100644 --- a/projectgraph/cha.go +++ b/projectgraph/cha.go @@ -185,7 +185,17 @@ func (b *builder) externalBase(class string) (string, bool) { return base, base != "" } +// findExternalBase searches the bases of the MRO of a class for an external +// one. The name of a base can resolve through a wildcard import of the class +// itself, as use self::Component::* in an impl of Component in Rust, and +// that lookup asks for the external base of the same class again. A class +// that the search reads now has no external base, so the search ends. func (b *builder) findExternalBase(class string) (string, bool) { + if b.findingBases[class] { + return "", false + } + b.findingBases[class] = true + defer delete(b.findingBases, class) for _, c := range b.mro(class) { for _, rec := range b.records(c) { for _, base := range b.bases(rec) { diff --git a/projectgraph/declared.go b/projectgraph/declared.go index c701c3c8..6dbb6c11 100644 --- a/projectgraph/declared.go +++ b/projectgraph/declared.go @@ -10,9 +10,10 @@ import ( // typedName returns the class or interface that the declared type of a // parameter or a typed local variable of the name names. The name belongs // to the scope or to a function that holds the scope, as the parameter of a -// method in an arrow function of the method. A Go local that holds the -// result of a call has the type of the first result of the function. -func (b *builder) typedName(sc *scope, scopeKey, name string) (string, bool) { +// method in an arrow function of the method. A Go local that holds a +// result of a call has the type of the result at its position. A name +// with no known type gives the reason of the record of a call on it. +func (b *builder) typedName(sc *scope, scopeKey, name string) (string, fileindex.UnresolvedReason, bool) { for key := scopeKey; fileindex.HasKeyPrefix(key, sc.ix.Path) && key != sc.ix.Path; key = fileindex.Parent(key) { def := b.defs[key] if def == nil { @@ -24,42 +25,41 @@ func (b *builder) typedName(sc *scope, scopeKey, name string) (string, bool) { continue } if !t.Result { - return b.resolveType(sc, key, t.Type) + class, ok := b.resolveType(sc, key, t.Type) + return class, fileindex.UnresolvedUnknownReceiver, ok } fn, ok := b.resolveScopeName(sc, t.Type) if !ok { - return "", false + return "", fileindex.UnresolvedUnknownReceiver, false } - return b.resultType(fn) + res, reason, ok := b.resultType(fileindex.PackageOf(sc.ix.Path), fn, t.Index) + // A name that holds a whole Option or Result has no + // type of a method of the value inside. + if ok && res.wraps != "" { + return "", fileindex.UnresolvedUnknownReceiver, false + } + return res.class, reason, ok } } } - return "", false + return "", fileindex.UnresolvedUnknownReceiver, false } // memberType returns the class of a name in a scope: a typed parameter or // local variable, or in Java a field of a class that holds the scope, which -// a method names with no this. -func (b *builder) memberType(sc *scope, scopeKey, name string) (string, bool) { - if class, ok := b.typedName(sc, scopeKey, name); ok || !sc.profile.implicitFields { - return class, ok +// a method names with no this. A name with no known type gives the reason +// of the record of a call on it. +func (b *builder) memberType(sc *scope, scopeKey, name string) (string, fileindex.UnresolvedReason, bool) { + class, reason, ok := b.typedName(sc, scopeKey, name) + if ok || !sc.profile.implicitFields { + return class, reason, ok } for class, ok := b.enclosingClass(scopeKey); ok; class, ok = b.enclosingClass(fileindex.Parent(class)) { if t, ok := b.fieldType(class, name); ok { - return t, true + return t, "", true } } - return "", false -} - -// resultType returns the class or interface that the first result of a Go -// function names. -func (b *builder) resultType(fn string) (string, bool) { - def := b.defs[fn] - if def == nil || def.Returns == "" { - return "", false - } - return b.resolveType(b.scopes[fileindex.Unit(fn)], fn, def.Returns) + return "", reason, false } // fieldType returns the class or interface that the declared type of a @@ -81,7 +81,11 @@ func (b *builder) fieldType(class, name string) (string, bool) { // first-party class or interface. A type of a dependency narrows nothing, as an // instance of a project class can extend it. A Go value holds its declared // type or, through an interface, a type of the project, so a Go type of a -// dependency gives the external key of its methods. +// dependency gives the external key of its methods. A type that a unit of +// dependency code gives narrows in any language with no subclass, as a +// Rust struct: a class gives its external key, and an interface or a trait +// gives its definition, whose implementations the class hierarchy analysis +// reads. func (b *builder) resolveType(sc *scope, from, text string) (string, bool) { if sc == nil { return "", false @@ -92,12 +96,44 @@ func (b *builder) resolveType(sc *scope, from, text string) (string, bool) { return "", false case b.isType(key): return key, true - case sc.profile.implicitInterfaces && b.kindOf(key) == "" && b.g.IsExternal(key) && strings.Contains(key, fileindex.Separator): - return key, true + case b.g.IsExternal(key) && strings.Contains(key, fileindex.Separator): + if t, ok := b.unitType(key); ok && (sc.profile.implicitInterfaces || sc.profile.unitTypes) { + return t, true + } + return key, sc.profile.implicitInterfaces && b.kindOf(key) == "" } return "", false } +// unitType returns the type that a unit of dependency code gives for an +// external key: the key itself for a class, or the definition of an +// interface or a trait. +func (b *builder) unitType(key string) (string, bool) { + if t, ok := b.unitTypes[key]; ok { + return t, t != "" + } + t := "" + defs, _ := b.unitDefinitions(b.origins[key]) + for _, d := range defs { + switch b.kindOf(d) { + case NodeInterface: + t = d + case NodeClass: + t = key + default: + continue + } + break + } + // unitDefinitions skips a name that an outer walk resolves now, so an + // answer found under the cycle guard can miss a definition. It stays + // out of the cache, or a later query would get the miss. + if len(b.stepping) == 0 { + b.unitTypes[key] = t + } + return t, t != "" +} + // dispatches reports whether a receiver of the declared type can hold an // instance of another type, so a call on it in a file of the language can // run the method of that type. A Go value of a named type that is not an diff --git a/projectgraph/golang.go b/projectgraph/golang.go index f1e390e8..c6ccc4eb 100644 --- a/projectgraph/golang.go +++ b/projectgraph/golang.go @@ -22,6 +22,8 @@ var goProfile = profile{ provide: goProvide, narrowProviders: goNarrowProviders, surface: goSurface, + symbol: goSymbol, + abstractReceiver: fileindex.UnresolvedInterfaceReceiver, dependencies: func(b *builder) ([]string, []modules.Problem) { return b.project.golang.Dependencies(), nil }, implicitInterfaces: true, typeConversions: true, @@ -30,10 +32,21 @@ var goProfile = profile{ } // goProvide returns the package unit of a Go module that an import path -// under its module path names. +// under its module path names. The unit of the standard library of +// GOROOT/src, as the unit go:std@go1.25.1, gives each import path whose +// first element has no dot. func goProvide(_ *builder, sp *space, module string, _ modules.Form) []provision { + if sp.pkg.Ecosystem != "go" { + return nil + } rel, ok := strings.CutPrefix(module, sp.pkg.Name) - if sp.pkg.Ecosystem != "go" || !ok || rel != "" && !strings.HasPrefix(rel, "/") { + switch { + case sp.pkg.Name == modules.StdModule: + if !modules.IsStdPath(module) { + return nil + } + rel = module + case !ok || rel != "" && !strings.HasPrefix(rel, "/"): return nil } dir, ok := sp.golang.Resolve(module) @@ -82,6 +95,18 @@ func goSurface(s *surfacer) { } } +// goSymbol returns the import path of the package of a Go definition of a +// unit and the names of the definition, as golang.org/x/net/html and +// Tokenizer, Next. +func goSymbol(b *builder, sp *space, def string) (string, []string, bool) { + dir := modules.Dir(sp.inner(fileindex.Unit(def))) + if sp.goPackages[dir] == nil { + return "", nil, false + } + importPath, ok := b.goImportPath(sp, dir) + return importPath, fileindex.Segments(def), ok +} + // goImportPath returns the import path of a Go package of a unit. A package // under an internal directory and a main package have no importer outside // the module. diff --git a/projectgraph/graph.go b/projectgraph/graph.go index 00c72caf..09f7bd51 100644 --- a/projectgraph/graph.go +++ b/projectgraph/graph.go @@ -17,7 +17,7 @@ import ( // Version is the version of the link rules. Bump it when a rule that makes a // node, an edge, a root or an unresolved site changes, so a diff of two // reports shows the change. -const Version = "11" +const Version = "16" type NodeKind string @@ -163,7 +163,10 @@ func (s Site) String() string { } // Node is a definition, a module top level, a re-exported name or an -// external target. An external node has only its key and kind. +// external target. An external node has only its key and kind. External +// is the key that an importer of the package writes for a function, a +// method or a constructor of a unit of dependency code, as +// golang.org//x//net//html//render1. type Node struct { Key string `json:"key"` Kind NodeKind `json:"kind"` @@ -172,6 +175,7 @@ type Node struct { Exported bool `json:"exported,omitempty"` Test bool `json:"test,omitempty"` Flags []fileindex.Flag `json:"flags,omitempty"` + External string `json:"external,omitempty"` } // Edge is one resolved step from a caller to a callee. Resolver names the diff --git a/projectgraph/imports.go b/projectgraph/imports.go index 32c25345..85a930d0 100644 --- a/projectgraph/imports.go +++ b/projectgraph/imports.go @@ -74,7 +74,7 @@ func (b *builder) importedModules(ix *fileindex.Index, spec string, form modules } var mods []moduleRef var missing []string - for _, pv := range b.providers(origin{lang: ix.Language, unit: ix.Unit, module: spec, form: form}) { + for _, pv := range b.providers(origin{lang: ix.Language, unit: ix.Unit, module: b.packageModule(ix, spec), form: form}) { if pv.module == "" { missing = append(missing, pv.sp.unit) continue diff --git a/projectgraph/link.go b/projectgraph/link.go index 790fa88b..4f511f41 100644 --- a/projectgraph/link.go +++ b/projectgraph/link.go @@ -123,17 +123,24 @@ type builder struct { // defs holds the definition records by key. defs map[string]*fileindex.Definition // reading holds the variables whose value a walk reads now, so a cycle - // of variables across files stops. - reading map[string]bool + // of variables across files stops. stepping holds the names that a + // walk of a unit reads now for the result type of an external key. + reading map[string]bool + stepping map[nameKey]bool + // unitTypes holds the type that a unit of dependency code gives for + // each external key, or "". + unitTypes map[string]string // groupMembers holds the members of each group of aliases by its first // alias, and nodesByParent the keys of the nodes by their parent. groupMembers map[string]map[string]string nodesByParent map[string][]string // groupRecords holds the class records of each group of aliases, and - // externalBases the first external base of each class, or "". + // externalBases the first external base of each class, or "", and + // findingBases the classes whose external base a search reads now. groupRecords map[string][]classRecord externalBases map[string]string + findingBases map[string]bool // state holds the state of each language that keeps one, as the crates // of Rust. langState reads it. state map[langcode.Code]any @@ -248,33 +255,36 @@ func newTables(indexes []*fileindex.Index, opts Options) *builder { Nodes: make(map[string]*Node, defs), Files: make(map[string]fileindex.FileStatus, len(indexes)), }, - byPath: make(map[string]*fileindex.Index, len(indexes)), - scopes: make(map[string]*scope, len(indexes)), - edges: newEdgeSet(sites), - unresolved: map[UnresolvedSite]bool{}, - truncations: map[Truncation]bool{}, - names: map[nameKey]resolution{}, - visiting: map[nameKey]bool{}, - bindings: map[string]string{}, - classes: map[string]classRecord{}, - defs: make(map[string]*fileindex.Definition, defs), - reading: map[string]bool{}, - state: map[langcode.Code]any{}, - units: map[string]*unit{}, - unitOf: map[string]*unit{}, - spaces: map[string]*space{}, - origins: map[string][]origin{}, - provisions: map[provisionKey][]provision{}, - linked: map[string]bool{}, - named: map[string]bool{}, - namespaces: map[langcode.Code]*namespaces{}, - loads: map[string]bool{}, - mros: map[string][]string{}, - aliases: map[string][]string{}, - narrowed: map[Site][]string{}, - structural: map[siteCall]bool{}, - test: map[string]bool{}, - rules: roots.Merge(roots.Default(), opts.Rules), + byPath: make(map[string]*fileindex.Index, len(indexes)), + scopes: make(map[string]*scope, len(indexes)), + edges: newEdgeSet(sites), + unresolved: map[UnresolvedSite]bool{}, + truncations: map[Truncation]bool{}, + names: map[nameKey]resolution{}, + visiting: map[nameKey]bool{}, + bindings: map[string]string{}, + classes: map[string]classRecord{}, + defs: make(map[string]*fileindex.Definition, defs), + reading: map[string]bool{}, + stepping: map[nameKey]bool{}, + unitTypes: map[string]string{}, + findingBases: map[string]bool{}, + state: map[langcode.Code]any{}, + units: map[string]*unit{}, + unitOf: map[string]*unit{}, + spaces: map[string]*space{}, + origins: map[string][]origin{}, + provisions: map[provisionKey][]provision{}, + linked: map[string]bool{}, + named: map[string]bool{}, + namespaces: map[langcode.Code]*namespaces{}, + loads: map[string]bool{}, + mros: map[string][]string{}, + aliases: map[string][]string{}, + narrowed: map[Site][]string{}, + structural: map[siteCall]bool{}, + test: map[string]bool{}, + rules: roots.Merge(roots.Default(), opts.Rules), } } @@ -437,6 +447,7 @@ func (b *builder) copyUnresolved(ix *fileindex.Index) { } func (b *builder) finish() *Graph { + b.markExternals() g := b.g g.Edges = b.edges.sorted() g.Unresolved = sortedKeys(b.unresolved, compareUnresolved) diff --git a/projectgraph/modules/cargo.go b/projectgraph/modules/cargo.go index 7d6dc85a..c4084706 100644 --- a/projectgraph/modules/cargo.go +++ b/projectgraph/modules/cargo.go @@ -18,10 +18,13 @@ type Cargo struct { problems problems } -// Crate is one package of a Cargo.toml. +// Crate is one package of a Cargo.toml. Renames maps the name that the +// crate writes for a dependency to the name of its package, as helper to +// kit for helper = { package = "kit" }. type Crate struct { - Dir string - Name string + Dir string + Name string + Renames map[string]string } // NewCargo reads the Cargo.toml of each directory of the file set. The name @@ -41,10 +44,11 @@ func NewCargo(files *Files, root fs.FS) *Cargo { c.problems.add(name, err) continue } + pairs := ScanTOML(data) if crate := cargoCrate(data); crate != "" { - c.crates = append(c.crates, Crate{Dir: d, Name: strings.ReplaceAll(crate, "-", "_")}) + c.crates = append(c.crates, Crate{Dir: d, Name: crateName(crate), Renames: cargoRenames(pairs)}) } - for _, pair := range ScanTOML(data) { + for _, pair := range pairs { if name, ok := cargoDependency(pair); ok && !slices.Contains(c.dependencies, name) { c.dependencies = append(c.dependencies, name) } @@ -70,6 +74,43 @@ func cargoCrate(data []byte) string { return cmp.Or(names["lib"], names["package"]) } +// cargoRenames returns the dependencies that a Cargo.toml renames with the +// package key, inline as helper = { package = "kit" } or in a +// [dependencies.helper] table. Both names are as a path writes them. +func cargoRenames(pairs []TOMLPair) map[string]string { + var renames map[string]string + for _, pair := range pairs { + name, ok := cargoDependency(pair) + if !ok { + continue + } + pkg := "" + if inline, ok := TOMLInlineTable(pair.Value); ok && name == pair.Key { + for _, p := range inline { + if p.Key == "package" { + pkg = p.Value + } + } + } else if pair.Key == "package" && name != pair.Key { + pkg, _ = TOMLString(pair.Value) + } + if pkg == "" { + continue + } + if renames == nil { + renames = map[string]string{} + } + renames[crateName(name)] = crateName(pkg) + } + return renames +} + +// crateName returns the name that a path writes for a package, as +// serde_json for serde-json. +func crateName(name string) string { + return strings.ReplaceAll(name, "-", "_") +} + // Problems returns the Cargo.toml files that the reader could not read. func (c *Cargo) Problems() []Problem { return c.problems } @@ -86,6 +127,17 @@ func (c *Cargo) Crate(file string) (Crate, bool) { return Crate{}, false } +// Rename returns the package that a file names by a crate name: the +// package of a rename in the Cargo.toml of the file, or else the name. +func (c *Cargo) Rename(file, name string) string { + if crate, ok := c.Crate(file); ok { + if pkg, ok := crate.Renames[name]; ok { + return pkg + } + } + return name +} + // Has reports whether a crate of the project has the name. func (c *Cargo) Has(name string) bool { return slices.ContainsFunc(c.crates, func(crate Crate) bool { return crate.Name == name }) diff --git a/projectgraph/modules/cargo_test.go b/projectgraph/modules/cargo_test.go index 5404604d..812b8714 100644 --- a/projectgraph/modules/cargo_test.go +++ b/projectgraph/modules/cargo_test.go @@ -32,3 +32,22 @@ func TestCargoReadsTheCratesOfTheManifests(t *testing.T) { assert.False(t, c.Has("web_app")) assert.Empty(t, c.Problems()) } + +func TestCargoReadsTheRenamesOfADependency(t *testing.T) { + root := fstest.MapFS{ + "Cargo.toml": {Data: []byte("[package]\nname = \"app\"\n\n[dependencies]\nhelper = { package = \"kit-core\", version = \"1\" }\nserde = \"1\"\n\n[target.'cfg(unix)'.dependencies.sys]\npackage = \"kit-sys\"\nversion = \"1\"\n")}, + "src/main.rs": {Data: []byte("")}, + } + c := NewCargo(NewFiles([]string{"src/main.rs"}), root) + + for _, tc := range []struct { + name, want string + }{ + {"helper", "kit_core"}, + {"sys", "kit_sys"}, + {"serde", "serde"}, + {"kit_core", "kit_core"}, + } { + assert.Equal(t, tc.want, c.Rename("src/main.rs", tc.name), tc.name) + } +} diff --git a/projectgraph/modules/golang.go b/projectgraph/modules/golang.go index ae6a3edc..42d2db94 100644 --- a/projectgraph/modules/golang.go +++ b/projectgraph/modules/golang.go @@ -12,6 +12,18 @@ import ( var errNoModule = errors.New("go.mod has no module directive") +// StdModule is the module path of the standard library in GOROOT/src. The +// import path of each of its packages is the directory, as net/http, with +// no module prefix. +const StdModule = "std" + +// IsStdPath reports whether an import path can name a package of the +// standard library: its first element has no dot. +func IsStdPath(spec string) bool { + first, _, _ := strings.Cut(spec, "/") + return first != "" && !strings.Contains(first, ".") +} + // goIgnoredDirs hold Go code that is not a package of the module. vendor // holds the code of a dependency, which stays external, and the go command // skips testdata. @@ -94,7 +106,9 @@ func (g *Golang) Resolve(spec string) (string, bool) { // directory of the project can name it. func (g *Golang) FirstParty(spec string) bool { if len(g.modules) > 0 { - return slices.ContainsFunc(g.modules, func(m goModule) bool { return underPath(spec, m.path) }) + return slices.ContainsFunc(g.modules, func(m goModule) bool { + return underPath(spec, m.path) || m.path == StdModule && IsStdPath(spec) + }) } // Each suffix of the path after a slash is a lookup, because a scan of // the directories at each import grows with the square of the project. @@ -121,6 +135,9 @@ func (g *Golang) ImportPath(dir string) (string, bool) { if slices.ContainsFunc(strings.Split(rel, "/"), func(part string) bool { return slices.Contains(goIgnoredDirs, part) }) { return "", false } + if m.path == StdModule { + return rel, rel != "" + } return path.Join(m.path, rel), true } return "", false diff --git a/projectgraph/modules/golang_test.go b/projectgraph/modules/golang_test.go index f9578264..bcbfe080 100644 --- a/projectgraph/modules/golang_test.go +++ b/projectgraph/modules/golang_test.go @@ -67,3 +67,23 @@ func TestGolangWithNoModule(t *testing.T) { assert.False(t, g.FirstParty("fmt"), "the root directory names no import path") assert.Empty(t, g.Problems()) } + +func TestGolangStandardLibrary(t *testing.T) { + paths := []string{"net/http/client.go", "internal/poll/fd.go", "cmd/go/main.go", "vendor/golang.org/x/net/http2/h2.go"} + root := fstest.MapFS{ + "go.mod": {Data: []byte("module std\n\ngo 1.25\n")}, + "cmd/go.mod": {Data: []byte("module cmd\n\ngo 1.25\n")}, + } + g := NewGolang(NewFiles(paths), root, []string{"net/http", "internal/poll", "cmd/go", "vendor/golang.org/x/net/http2"}) + + dir, ok := g.Resolve("net/http") + assert.True(t, ok) + assert.Equal(t, "net/http", dir, "a package of std has its directory as its import path") + dir, ok = g.Resolve("cmd/go") + assert.True(t, ok) + assert.Equal(t, "cmd/go", dir) + _, ok = g.Resolve("golang.org/x/net/http2") + assert.False(t, ok, "vendor stays external") + assert.True(t, g.FirstParty("net/url")) + assert.False(t, g.FirstParty("golang.org/x/net/html")) +} diff --git a/projectgraph/packages.go b/projectgraph/packages.go index 3eb03a9d..a84e88d4 100644 --- a/projectgraph/packages.go +++ b/projectgraph/packages.go @@ -231,17 +231,32 @@ func (b *builder) recordOrigin(ix *fileindex.Index, key, module string, form mod if len(b.spaces) < 2 { return } - o := origin{lang: ix.Language, unit: ix.Unit, module: module, form: form, names: slices.Clone(names)} + o := origin{lang: ix.Language, unit: ix.Unit, module: b.packageModule(ix, module), form: form, names: slices.Clone(names)} if !slices.ContainsFunc(b.origins[key], o.equal) { b.origins[key] = append(b.origins[key], o) } } +// packageModule returns the module of the package that an import of the +// file names, by the profile of its family. +func (b *builder) packageModule(ix *fileindex.Index, module string) string { + if imported := profileOf(ix.Language.Family()).importedModule; imported != nil { + return imported(b, ix, module) + } + return module +} + // deriveExternal returns the external node of the names under an external // key. The origins of the key, with the names after their own, are the // origins of the new key. func (b *builder) deriveExternal(parent string, names []string) string { - key := b.external(fileindex.Join(parent, names...)) + return b.external(b.deriveKey(parent, names)) +} + +// deriveKey returns the key of the names under an external key, with the +// origins of the key and the names after their own, and makes no node. +func (b *builder) deriveKey(parent string, names []string) string { + key := fileindex.Join(parent, names...) for _, o := range b.origins[parent] { o.names = append(slices.Clone(o.names), names...) if !slices.ContainsFunc(b.origins[key], o.equal) { diff --git a/projectgraph/profile.go b/projectgraph/profile.go index 4eadb95f..eb2579df 100644 --- a/projectgraph/profile.go +++ b/projectgraph/profile.go @@ -47,6 +47,27 @@ type profile struct { // surface adds the exported symbols of a unit of dependency code with // the import that names each one. The profile of the family holds it. surface func(s *surfacer) + // importedModule returns the module of the package that an import of a + // file names, as kit::serve for helper::serve when the Cargo.toml of + // the file renames the package kit to helper. + importedModule func(b *builder, ix *fileindex.Index, module string) string + // symbol returns the module that an importer of a definition of a unit + // of dependency code imports, and the names of the definition under + // it, as golang.org/x/net/html and Tokenizer. The profile of the family + // holds it. + symbol func(b *builder, sp *space, def string) (string, []string, bool) + // unitTypes marks a language with no subclass of a type of another + // crate, as Rust, so a declared type that a unit of dependency code + // gives narrows a call to the method of the type. + unitTypes bool + // sameTypeMethods name the methods that give a value of the type of + // the receiver, as clone of Rust, also when the type derives them and + // the code defines none. + sameTypeMethods map[string]bool + // abstractReceiver is the reason of the record of a call on a value of + // an interface or a trait that no type of the graph implements, as + // trait-receiver of Rust. + abstractReceiver fileindex.UnresolvedReason // dependencies returns the packages that the manifests of the project // declare for the ecosystem of the family, and the manifests that the // reader could not read. The profile of the family holds it. @@ -171,6 +192,11 @@ type profile struct { // mainLoadsPackage marks a language whose program initializes the // package of main before main runs, as Go. mainLoadsPackage bool + // helperProgram reports whether the main of a file is a helper + // program of the project and not the program that its users run, as a + // build script or an example of a Rust crate. Its root stays, and it + // does not turn off the library-surface roots. + helperProgram func(path string) bool } // profiles are the languages of the link step, in a fixed order. The units diff --git a/projectgraph/reach/reach.go b/projectgraph/reach/reach.go index 6bdb4cfc..873e77b4 100644 --- a/projectgraph/reach/reach.go +++ b/projectgraph/reach/reach.go @@ -21,7 +21,9 @@ import ( // uncertainty, the tie rule of the paths, the caps and the fields of a // hop. Bump it when a rule changes, so a diff of two results shows the // change. Version 2 added the root kind to the first hop of a path. -const Version = "2" +// Version 3 matches a target against the external key of a definition of a +// unit of dependency code. +const Version = "3" const ( defaultMaxDepth = 64 @@ -358,6 +360,10 @@ type adjacency struct { out [][]int32 in [][]int32 externals []int + // unitDefs holds the definitions of units of dependency code with an + // external key, by id, and byExternal the same by the key. + unitDefs []int + byExternal map[string][]int includeTests bool @@ -392,11 +398,17 @@ func newAdjacency(g *projectgraph.Graph) *adjacency { slices.Sort(keys) keys = slices.Compact(keys) - a := &adjacency{g: g, keys: keys, id: make(map[string]int, len(keys)), out: make([][]int32, len(keys)), in: make([][]int32, len(keys))} + a := &adjacency{g: g, keys: keys, id: make(map[string]int, len(keys)), out: make([][]int32, len(keys)), in: make([][]int32, len(keys)), byExternal: map[string][]int{}} for i, k := range keys { a.id[k] = i - if n := g.Nodes[k]; n != nil && n.Kind == projectgraph.NodeExternal { + n := g.Nodes[k] + switch { + case n == nil: + case n.Kind == projectgraph.NodeExternal: a.externals = append(a.externals, i) + case n.External != "": + a.unitDefs = append(a.unitDefs, i) + a.byExternal[n.External] = append(a.byExternal[n.External], i) } } for i, e := range g.Edges { @@ -508,21 +520,31 @@ func (a *adjacency) methodReaches(name string, target Target) bool { return slices.ContainsFunc(a.match(target), func(id int) bool { return c[id] }) } -// match returns the external nodes of a target, by id. +// match returns the nodes of a target, by id: the external node of its +// key, and each definition of a unit of dependency code whose external key +// it names. A call inside a unit goes to the definition, so the search +// reaches a target that only the code of its unit calls through it. func (a *adjacency) match(target Target) []int { + var ids []int if _, ok := target.wildcard(); ok { - var ids []int for _, id := range a.externals { if target.Matches(a.keys[id]) { ids = append(ids, id) } } - return ids - } - if n := a.g.Nodes[target.Key()]; n != nil && n.Kind == projectgraph.NodeExternal { - return []int{a.id[target.Key()]} + for _, id := range a.unitDefs { + if target.Matches(a.g.Nodes[a.keys[id]].External) { + ids = append(ids, id) + } + } + } else { + if n := a.g.Nodes[target.Key()]; n != nil && n.Kind == projectgraph.NodeExternal { + ids = append(ids, a.id[target.Key()]) + } + ids = append(ids, a.byExternal[target.Key()]...) } - return nil + slices.Sort(ids) + return ids } func (a *adjacency) ids(roots []projectgraph.Root) []int { @@ -642,6 +664,11 @@ func (a *adjacency) callSites(ids []int, s *search) []CallSite { for _, id := range ids { for _, i := range a.in[id] { e := &a.g.Edges[i] + // A package edge joins the external key of a target to its + // definition. The calls of the key are its own call sites. + if e.Kind == projectgraph.EdgePackage { + continue + } sites = append(sites, CallSite{Caller: e.From, Site: e.Site, Reached: s.dist[a.id[e.From]] >= 0}) } } diff --git a/projectgraph/resolve.go b/projectgraph/resolve.go index 89ab2862..fd7e7b6f 100644 --- a/projectgraph/resolve.go +++ b/projectgraph/resolve.go @@ -90,7 +90,7 @@ func (b *builder) linkCalls(ix *fileindex.Index) { if r.unbound && sc.profile.thisCalls { r = b.thisCall(ix, caller, call, r) } - b.linkCHA(call, r, from, site) + overrides := b.linkCHA(call, r, from, site) // A declared type gives the class of the receiver, so the // link step owns the record of the site and no name edge // goes from it. @@ -98,13 +98,19 @@ func (b *builder) linkCalls(ix *fileindex.Index) { b.narrow(site, call.Callee) } if r.fail != nil { + reason := r.fail.reason + // A value of an interface or a trait that no type of the + // graph implements holds a type of code elsewhere. + if reason == fileindex.UnresolvedNoDefinition && overrides == 0 && r.declared && sc.profile.abstractReceiver != "" && b.kindOf(r.class) == NodeInterface { + reason = sc.profile.abstractReceiver + } b.addUnresolved(UnresolvedSite{ Caller: from, Site: site, Text: call.Callee, Receiver: call.Receiver, Method: cmp.Or(call.Method, r.fail.name), - Reason: r.fail.reason, + Reason: reason, }) continue } @@ -114,6 +120,11 @@ func (b *builder) linkCalls(ix *fileindex.Index) { if r.unbound && !b.unboundEdge(ix, call, from, site, r, unknown[[2]int{call.Line, call.Column}]) { continue } + // The index of some languages records such a call itself, and + // that record stays the one record of the site. + if reason, ok := b.onCallResult(sc.profile, call, r); ok && !unknown[[2]int{call.Line, call.Column}] { + b.addUnresolved(UnresolvedSite{Caller: from, Site: site, Text: call.Callee, Receiver: call.Receiver, Method: call.Method, Reason: reason}) + } switch { case r.kind == EdgeDirect && isAssignment(sc, call, r.target): r.kind, r.rule = EdgeAssignment, ruleAssignment @@ -128,6 +139,84 @@ func (b *builder) linkCalls(ix *fileindex.Index) { } } +// onCallResult returns the reason of the record of a call whose resolution +// took its method as a member of the external key of the called name of a +// call on a call result, as html//NewTokenizer//Next for +// html.NewTokenizer(r).Next(). The link step does not know the type of +// that result, so the key can name no symbol, and a target of the method +// of the real type would read as no-such-target. The edge stays, because +// the called name can be a type, as in a Go conversion +// http.HandlerFunc(f).ServeHTTP(w, r). The reason is no-dependency-source +// when no unit of dependency code gives the package of the key. +// +// A construction gives an instance of the called class, so the key is +// right: new Yaml().load() in Java, or Yaml().load() in Python or Kotlin, +// whose classes start with an upper case letter. A Go name starts with an +// upper case letter when the package exports it, so it tells nothing. +func (b *builder) onCallResult(p *profile, call fileindex.Call, r resolution) (fileindex.UnresolvedReason, bool) { + called, ok := calledName(call.Receiver) + if !ok || r.fail != nil || call.Method == "" || b.kindOf(r.key) != NodeExternal { + return "", false + } + if strings.HasPrefix(call.Receiver, "new ") || !p.typeConversions && startsUpper(called) { + return "", false + } + if fileindex.Last(r.key) != call.Method || fileindex.Last(fileindex.Parent(r.key)) != called { + return "", false + } + if _, provided := b.unitDefinitions(b.origins[r.key]); !provided { + return fileindex.UnresolvedNoDependencySource, true + } + return fileindex.UnresolvedUnknownReceiver, true +} + +// calledName returns the name of the call that ends a receiver as written, +// as NewTokenizer for html.NewTokenizer(r) and new for Vec::::new(). +func calledName(receiver string) (string, bool) { + head, ok := strings.CutSuffix(receiver, ")") + if !ok { + return "", false + } + for depth := 1; depth > 0; { + i := strings.LastIndexAny(head, "()") + if i < 0 { + return "", false + } + if head[i] == ')' { + depth++ + } else { + depth-- + } + head = head[:i] + } + head = strings.TrimSuffix(withoutTypeArguments(head), "::") + name := lastWritten(head) + return name, isIdentifier(name) +} + +// withoutTypeArguments removes the type arguments that end a callee as +// written, as [int] of New[int] in Go or of new:: in Rust. +func withoutTypeArguments(callee string) string { + closer := map[byte]byte{']': '[', '>': '<'} + if callee == "" || closer[callee[len(callee)-1]] == 0 { + return callee + } + end, open := callee[len(callee)-1], closer[callee[len(callee)-1]] + depth := 0 + for i := len(callee) - 1; i >= 0; i-- { + switch callee[i] { + case end: + depth++ + case open: + depth-- + if depth == 0 { + return callee[:i] + } + } + } + return callee +} + // thisCall resolves a call of a name that nothing binds in a language where // such a call runs on this, as a Java method that a base of a dependency // or an override in a subclass gives. A builtin function, as println of @@ -181,8 +270,10 @@ func (b *builder) unboundEdge(ix *fileindex.Index, call fileindex.Call, from str // step. // // A receiver that a call of the class makes, as p = Parser(), holds an -// instance of that class only, so it gets no cha edge. -func (b *builder) linkCHA(call fileindex.Call, r resolution, from string, site Site) { +// instance of that class only, so it gets no cha edge. An override in a +// unit of dependency code gets its edge through its external key. It +// returns the count of the overrides. +func (b *builder) linkCHA(call fileindex.Call, r resolution, from string, site Site) int { lang := b.byPath[site.Path].Language class, virtual := r.class, r.virtual // A Java call with no receiver runs on this. @@ -203,11 +294,11 @@ func (b *builder) linkCHA(call fileindex.Call, r resolution, from string, site S } switch { case !virtual || class == "" || call.Method == "" || !b.dispatches(lang, class): - return + return 0 case r.fail != nil && r.fail.name != call.Method: - return + return 0 case r.fail == nil && fileindex.Last(r.key) != call.Method: - return + return 0 } b.narrow(site, call.Callee) found := "" @@ -216,7 +307,7 @@ func (b *builder) linkCHA(call fileindex.Call, r resolution, from string, site S } overrides := b.overrides(class, call, found) for _, key := range overrides { - b.link(lang, from, key, site, EdgeCHA, ruleCHA) + b.link(lang, from, b.entryKey(from, key), site, EdgeCHA, ruleCHA) } // An ECMAScript type is structural. A value of an interface type can be // an object literal or an instance of a class with no implements, and an @@ -225,6 +316,7 @@ func (b *builder) linkCHA(call fileindex.Call, r resolution, from string, site S if profileOf(lang).structuralTypes && (b.kindOf(class) == NodeInterface || r.fail != nil && len(overrides) == 0) { b.structural[siteCall{site, call.Callee}] = true } + return len(overrides) } // linkReferences adds a reference edge from the definition that holds each @@ -286,7 +378,7 @@ func (b *builder) resolveTarget(sc *scope, caller, target string) []resolution { return []resolution{b.resolveLocal(sc, caller, target)} } if name, rest, ok := strings.Cut(target, fileindex.Separator); ok { - if class, ok := b.memberType(sc, caller, name); ok { + if class, _, ok := b.memberType(sc, caller, name); ok { return []resolution{b.walkTyped(class, strings.Split(rest, fileindex.Separator))} } } @@ -295,7 +387,7 @@ func (b *builder) resolveTarget(sc *scope, caller, target string) []resolution { names := append(lead, rest...) mods := b.resolveModules(ix, module, formOf(imp)) if len(mods) == 0 { - return []resolution{{key: b.importedExternal(ix, target, module, formOf(imp), names), kind: EdgeDirect, rule: ruleExternal}} + return []resolution{b.importedWalk(ix, target, module, formOf(imp), names)} } if len(names) == 0 && sc.profile.defaultExport != "" { names = []string{sc.profile.defaultExport} @@ -453,8 +545,9 @@ func (b *builder) resolveLocal(sc *scope, caller, target string) resolution { } r := b.walk(prefix, segs[i:]) if fileindex.HasKeyPrefix(caller, prefix) && b.isFunction(prefix) { - r = resolution{fail: &failure{reason: fileindex.UnresolvedUnknownReceiver, name: segs[i]}} - if class, ok := b.memberType(sc, prefix, segs[i]); ok { + class, reason, ok := b.memberType(sc, prefix, segs[i]) + r = resolution{fail: &failure{reason: reason, name: segs[i]}} + if ok { r = b.walkTyped(class, segs[i+1:]) } } diff --git a/projectgraph/resolve_test.go b/projectgraph/resolve_test.go new file mode 100644 index 00000000..aa54ba28 --- /dev/null +++ b/projectgraph/resolve_test.go @@ -0,0 +1,36 @@ +package projectgraph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalledName(t *testing.T) { + tests := []struct { + receiver string + name string + ok bool + }{ + {"html.NewTokenizer(r)", "NewTokenizer", true}, + {"html.NewTokenizer(io.MultiReader(a, b))", "NewTokenizer", true}, + {"dep.NewThing().Next()", "Next", true}, + {"pkg.New[int](x)", "New", true}, + {"Vec::::new()", "new", true}, + {"smallvec::new_thing()", "new_thing", true}, + {"yaml.load(f)", "load", true}, + {"z", "", false}, + {"a.b", "", false}, + {"(a)", "", false}, + {"f(x))", "", false}, + } + for _, tt := range tests { + t.Run(tt.receiver, func(t *testing.T) { + name, ok := calledName(tt.receiver) + assert.Equal(t, tt.ok, ok) + if tt.ok { + assert.Equal(t, tt.name, name) + } + }) + } +} diff --git a/projectgraph/results.go b/projectgraph/results.go new file mode 100644 index 00000000..ec2e09de --- /dev/null +++ b/projectgraph/results.go @@ -0,0 +1,269 @@ +package projectgraph + +import ( + "slices" + "strings" + + "github.com/safedep/code/fileindex" + "github.com/safedep/code/projectgraph/modules" +) + +// result is the class or the interface of a result of a function, as a +// file of an importer names it, and the Option or the Result of std that +// holds a Rust result. +type result struct { + class string + wraps string +} + +// resultType returns the result at a position of a function, as a file of +// the unit of the importer sees it, "" for the project. A function of a +// package that the project imports is an external key, and the unit of +// dependency code that gives the package gives its definition. A function +// of a package that no unit gives has no known result, so a call on the +// result is uncertain with the reason no-dependency-source. +func (b *builder) resultType(unit, fn string, pos int) (result, fileindex.UnresolvedReason, bool) { + defs := []string{fn} + if b.defs[fn] == nil && b.g.IsExternal(fn) { + var provided bool + defs, provided = b.unitDefinitions(b.origins[fn]) + if !provided { + return result{}, fileindex.UnresolvedNoDependencySource, false + } + } + if res, ok := b.oneResult(unit, defs, pos); ok { + return res, "", true + } + return result{}, fileindex.UnresolvedUnknownReceiver, false +} + +// oneResult returns the result at a position that all the definitions and +// their twins give. The platform files of a Go package can define one +// function twice with two result types. The walk follows one type, and a +// method of the other type would then be not-found, so two results give +// none. +func (b *builder) oneResult(unit string, defs []string, pos int) (result, bool) { + var all []string + for _, d := range defs { + all = append(append(all, d), b.twins(d)...) + } + var out result + for _, d := range all { + def := b.defs[d] + text := resultText(def, pos) + if text == "" { + continue + } + class, ok := b.resolveType(b.scopes[fileindex.Unit(d)], d, text) + if !ok { + return result{}, false + } + res := result{class: b.typeFor(unit, class), wraps: def.Wraps} + if out.class != "" && res != out { + return result{}, false + } + out = res + } + return out, out.class != "" +} + +// unwrap returns how many of the names after a call a wrapped result +// takes: one for unwrap or expect, which give the value inside, and none +// for a name that the value inside takes after a ?, which the index does +// not record. A method of the wrapper itself, as map of Option, gives no +// type, so it reports false. A result with no wrapper takes no name. Only +// a Rust result has a wrapper. +func unwrap(wraps string, next []string) (int, bool) { + if wraps == "" || len(next) == 0 { + return 0, true + } + return rustUnwrap(wraps, next[0]) +} + +// resultText returns the type name of the result at a position of a +// function, as written. +func resultText(def *fileindex.Definition, pos int) string { + switch { + case def == nil: + return "" + case pos < len(def.Results): + return def.Results[pos] + case pos == 0: + return def.Returns + } + return "" +} + +// unitDefinitions returns the definitions that the units of dependency +// code give for the origins of an external key, and whether a unit gives +// the package of an origin at all. A walk of a unit can ask for the +// definitions of another external key, so a key that a walk reads now +// gives none. +func (b *builder) unitDefinitions(origins []origin) ([]string, bool) { + var defs []string + provided := false + for _, o := range origins { + for _, pv := range b.providers(o) { + provided = true + switch { + case pv.module == "": + continue + case len(o.names) == 0: + // A Rust use can name the item itself, as use dep::Thing. + if b.defs[pv.module] != nil && !slices.Contains(defs, pv.module) { + defs = append(defs, pv.module) + } + continue + } + k := nameKey{module: pv.module, name: fileindex.Join("", o.names...)} + if b.stepping[k] { + continue + } + b.stepping[k] = true + r := b.walk(pv.module, o.names) + delete(b.stepping, k) + if r.fail == nil && b.defs[r.key] != nil && !slices.Contains(defs, r.key) { + defs = append(defs, r.key) + } + } + } + return defs, provided +} + +// externalResult returns the result of the function or the method of the +// name under an external key, as golang.org//x//net//html//Tokenizer for +// NewTokenizer under golang.org//x//net//html. A unit of dependency code +// gives the function, and the type is the external key of a class. A method +// of the same-type methods of the language, as clone in Rust, on a type of +// a unit keeps the type, also when the unit derives it and defines none. +func (b *builder) externalResult(parent, name string) (result, bool) { + origins := derivedOrigins(b.origins[parent], name) + if len(origins) == 0 { + return result{}, false + } + defs, _ := b.unitDefinitions(origins) + // The walk reads an external key, so the code that names it is in + // another space than the unit, and the type is its key. + if res, ok := b.oneResult("", slices.DeleteFunc(defs, func(d string) bool { return !b.isFunction(d) }), 0); ok { + return res, true + } + if profileOf(origins[0].lang).sameTypeMethods[name] { + types, _ := b.unitDefinitions(b.origins[parent]) + if slices.ContainsFunc(types, b.isType) { + return result{class: parent}, true + } + } + return result{}, false +} + +// derivedOrigins returns the origins of the name under a key with the +// origins. +func derivedOrigins(origins []origin, names ...string) []origin { + out := make([]origin, 0, len(origins)) + for _, o := range origins { + o.names = append(slices.Clone(o.names), names...) + out = append(out, o) + } + return out +} + +// typeFor returns the key of a class as a file of a unit names it, "" for +// the project. +// A class of a unit of dependency code that the importer does not belong +// to is its external key, as golang.org//x//net//html//Tokenizer, so a call +// of its method gets the external key that a target names, and a package +// edge then joins the key to the definition. An interface stays as it is, +// because the class hierarchy analysis reads its implementations. +func (b *builder) typeFor(unit, class string) string { + if fileindex.PackageOf(class) == unit || b.kindOf(class) == NodeInterface || !b.isType(class) { + return class + } + if key, ok := b.symbolKey(unit, class); ok { + return key + } + return class +} + +// entryKey returns the key of a definition that a caller runs: the +// definition itself, or for a definition of a unit of dependency code that +// the caller does not belong to, its external key with a node. The +// package edge of the key then joins it to the definition, and a target +// of the key finds the path. +func (b *builder) entryKey(from, def string) string { + if sameUnit(from, def) { + return def + } + if key, ok := b.symbolKey(fileindex.PackageOf(from), def); ok { + return b.external(key) + } + return def +} + +// symbolKey returns the external key of a definition of a unit of +// dependency code, as an import of its package names it, and records the +// import as an origin of the key for a file of the importer unit. +func (b *builder) symbolKey(importer, def string) (string, bool) { + key, o, ok := b.unitSymbol(def) + if !ok { + return "", false + } + o.unit = importer + if !slices.ContainsFunc(b.origins[key], o.equal) { + b.origins[key] = append(b.origins[key], o) + } + return key, true +} + +// unitSymbol returns the external key of a definition of a unit of +// dependency code, and the origin of an import that names it, with no +// importer unit. +func (b *builder) unitSymbol(def string) (string, origin, bool) { + unit := fileindex.PackageOf(def) + sp := b.spaces[unit] + if unit == "" || sp == nil { + return "", origin{}, false + } + lang := b.languageOf(def) + symbol := profileOf(lang.Family()).symbol + if symbol == nil { + return "", origin{}, false + } + module, names, ok := symbol(b, sp, def) + if !ok { + return "", origin{}, false + } + return fileindex.Join(fileindex.Namespace(lang, module), names...), origin{lang: lang, module: module, form: modules.FormImport, names: names}, true +} + +// markExternals sets the external key of each function, method and +// constructor of a unit of dependency code. A call that the unit makes to +// its own code goes to the definition and not to the external key, so +// reach matches a target against the external key of the definition too. +// Without it, a target that only the code of its unit calls reads as +// not-found, as the methods of the HTTP/2 server of x/net. +func (b *builder) markExternals() { + for _, n := range b.g.Nodes { + switch n.Kind { + case NodeFunction, NodeMethod, NodeConstructor: + default: + continue + } + if key, _, ok := b.unitSymbol(n.Key); ok { + n.External = key + } + } +} + +// importedWalk resolves the names under an import that no file of the +// space of the importer gives, as html//NewTokenizer//Next for +// html.NewTokenizer(r).Next(). The walk reads the names one at a time from +// the key of the module, so the result type of a function that a unit of +// dependency code gives types the next name. +func (b *builder) importedWalk(ix *fileindex.Index, target, module string, form modules.Form, names []string) resolution { + base, ok := strings.CutSuffix(target, fileindex.Join("", names...)) + if len(names) < 2 || !ok { + return resolution{key: b.importedExternal(ix, target, module, form, names), kind: EdgeDirect, rule: ruleExternal} + } + b.recordOrigin(ix, base, module, form, nil) + return b.walkFrom(resolution{key: base, kind: EdgeDirect, rule: ruleExternal}, names) +} diff --git a/projectgraph/roots.go b/projectgraph/roots.go index 93466abd..33483fad 100644 --- a/projectgraph/roots.go +++ b/projectgraph/roots.go @@ -50,7 +50,8 @@ func (b *builder) markRoots(ctx context.Context) error { entry := false for _, r := range found { b.addRootAt(r.Key, r.Kind, r.Rule, Site(r.Site)) - entry = entry || r.Kind != RootTest + helper := b.profileAt(r.Key).helperProgram + entry = entry || r.Kind != RootTest && (helper == nil || !helper(fileindex.Unit(r.Key))) // A Go program initializes its package before main runs, so the // module top level of each file of the package is a root too. if u := b.unitOf[fileindex.Unit(r.Key)]; r.Kind == RootMain && u != nil && b.profileAt(r.Key).mainLoadsPackage { diff --git a/projectgraph/rust.go b/projectgraph/rust.go index a589c0dc..aacb25ea 100644 --- a/projectgraph/rust.go +++ b/projectgraph/rust.go @@ -1,6 +1,7 @@ package projectgraph import ( + "slices" "strings" "github.com/safedep/code/fileindex" @@ -16,12 +17,20 @@ import ( // impl of a trait makes the trait a base. var rustProfile = profile{ code: langcode.Rust, - resolver: func(b *builder, _ *space) modules.Resolver { return rustResolver{b} }, + resolver: func(b *builder, sp *space) modules.Resolver { return rustResolver{b: b, sp: sp} }, units: (*builder).addRustUnits, + dependencyCode: true, aliases: (*builder).joinImplBlocks, link: (*builder).linkRust, scopeFallback: (*builder).rustScopeFallback, - dependencies: func(b *builder) ([]string, []modules.Problem) { return b.rustState().cargo.Dependencies(), nil }, + provide: rustProvide, + importedModule: rustImportedModule, + symbol: rustSymbol, + surface: rustSurface, + unitTypes: true, + sameTypeMethods: map[string]bool{"clone": true, "to_owned": true, "default": true, "from": true}, + abstractReceiver: fileindex.UnresolvedTraitReceiver, + dependencies: func(b *builder) ([]string, []modules.Problem) { return b.cargoOf(b.project).Dependencies(), nil }, instance: []string{"self"}, superModule: true, unitImports: true, @@ -33,6 +42,23 @@ var rustProfile = profile{ implicitMethods: rustImplicit, forwardMethod: "deref", methodAliases: rustConversions, + helperProgram: rustHelperProgram, +} + +// rustHelperProgram reports whether a file is the build script, an example +// or a benchmark of a crate. A library crate with a build.rs has a main, +// but its users call its public items. Without this, the build script +// made the only root of the crate, and a call of the library read as +// not-found. +func rustHelperProgram(path string) bool { + dir, file := "", path + if i := strings.LastIndex(path, "/"); i >= 0 { + dir, file = path[:i], path[i+1:] + } + if file == "build.rs" { + return true + } + return slices.ContainsFunc(strings.Split(dir, "/"), func(d string) bool { return d == "examples" || d == "benches" }) } // rustConversions name the methods of the traits of std that a call of @@ -69,28 +95,82 @@ var rustImplicit = map[string]bool{ "call_once": true, "serialize": true, "deserialize": true, } +// rustUnwraps are the methods of Option and Result that give the value +// inside, and rustWrapperMethods the other methods of each one. A ? +// gives the value inside too, and the index records no ?, so a name that +// neither table holds takes the value inside. +var ( + rustUnwraps = map[string]bool{"unwrap": true, "expect": true, "unwrap_unchecked": true} + rustWrapperMethods = map[string]map[string]bool{ + "Option": setOf("and", "and_then", "as_deref", "as_deref_mut", "as_mut", "as_ref", "cloned", "copied", "filter", + "flatten", "get_or_insert", "get_or_insert_with", "insert", "is_none", "is_some", "is_some_and", "iter", + "iter_mut", "map", "map_or", "map_or_else", "ok_or", "ok_or_else", "or", "or_else", "replace", "take", + "unwrap_or", "unwrap_or_default", "unwrap_or_else", "xor", "zip"), + "Result": setOf("and", "and_then", "as_deref", "as_deref_mut", "as_mut", "as_ref", "cloned", "copied", "err", + "expect_err", "flatten", "is_err", "is_ok", "is_ok_and", "iter", "iter_mut", "map", "map_err", "map_or", + "map_or_else", "ok", "or", "or_else", "unwrap_err", "unwrap_or", "unwrap_or_default", "unwrap_or_else"), + } +) + +func setOf(names ...string) map[string]bool { + out := make(map[string]bool, len(names)) + for _, n := range names { + out[n] = true + } + return out +} + +// rustUnwrap returns how many names a Rust result in the wrapper takes +// before the next name: one for an unwrap, none for a name of the value +// inside, and false for a method of the wrapper itself. +func rustUnwrap(wraps, next string) (int, bool) { + switch { + case rustUnwraps[next]: + return 1, true + case rustWrapperMethods[wraps][next]: + return 0, false + } + return 0, true +} + // rustRoots are the first names of the paths of the crates of the // toolchain, which no crate of the project defines. var rustRoots = map[string]bool{"std": true, "core": true, "alloc": true} -// rustState is the state of Rust in a build: the crates, and the module -// path of each Rust file. +// rustState is the state of Rust in a build: the crates of each space, the +// module path of each Rust file, and the paths that a resolution reads now, +// by the file that writes them. type rustState struct { - cargo *modules.Cargo - modules map[string]string + cargo map[*space]*modules.Cargo + modules map[string]string + resolving map[[2]string]bool } func (b *builder) rustState() *rustState { return langState[rustState](b, langcode.Rust) } +// cargoOf returns the crates of the Cargo.toml files of a space. +func (b *builder) cargoOf(sp *space) *modules.Cargo { + s := b.rustState() + if s.cargo == nil { + s.cargo = map[*space]*modules.Cargo{} + } + c, ok := s.cargo[sp] + if !ok { + c = modules.NewCargo(sp.files, sp.root) + s.cargo[sp] = c + b.addProblems(sp, c.Problems()) + } + return c +} + // addRustUnits makes each Rust file the unit of its module, in the table of // the module paths. func (b *builder) addRustUnits() { s := b.rustState() - s.cargo = modules.NewCargo(b.project.files, b.opts.Root) - b.problems = append(b.problems, s.cargo.Problems()...) s.modules = map[string]string{} + s.resolving = map[[2]string]bool{} files := map[string][]string{} for _, p := range b.paths { if ix := b.byPath[p]; ix != nil && ix.Language == langcode.Rust { @@ -112,15 +192,25 @@ func (b *builder) addRustUnits() { // with no Cargo.toml. src/lib.rs and src/main.rs are the root of the crate. // A file of src/bin, tests, examples or benches, and build.rs, is the root // of a crate of its own, which no path of another file names. +// +// The root of a crate of a unit of dependency code is the unit, as +// cargo:smallvec@1.13.2::alloc for src/alloc.rs of smallvec. So the +// modules of two versions of one crate, or of a crate of the project with +// the name of a dependency, stay apart. func (b *builder) rustModule(p string) string { - crate, ok := b.rustState().cargo.Crate(p) - name, rel := "crate", p + sp := b.spaceOf(p) + inner := sp.inner(p) + crate, ok := b.cargoOf(sp).Crate(inner) + name, rel := "crate", inner if ok { name = crate.Name if crate.Dir != "" { - rel = strings.TrimPrefix(p, crate.Dir+"/") + rel = strings.TrimPrefix(inner, crate.Dir+"/") } } + if sp.unit != "" { + name = sp.unit + } src, inSrc := strings.CutPrefix(rel, "src/") first, _, _ := strings.Cut(rel, "/") switch { @@ -161,20 +251,140 @@ func (b *builder) rustPaths(module, written string) []string { return []string{joinPath(module, parts)} } out := []string{joinPath(module, parts)} - if b.rustState().cargo.Has(parts[0]) { + switch sp := b.rustSpaceOf(module); { + case sp.unit != "" && b.rustCrateName(sp) == parts[0]: + out = append(out, joinPath(sp.unit, parts[1:])) + case sp.unit == "" && b.cargoOf(sp).Has(parts[0]): out = append(out, written) } return out } +// rustSpaceOf returns the space of a module path: the unit of its root, or +// the project. +func (b *builder) rustSpaceOf(module string) *space { + root, _, _ := strings.Cut(module, "::") + if sp := b.spaces[root]; sp != nil { + return sp + } + return b.project +} + +// rustCrateName returns the name that a path of another crate writes for +// the crate of a unit of dependency code, as tokio_postgres for the package +// tokio-postgres: the name of its root Cargo.toml, or else the name of its +// package. +func (b *builder) rustCrateName(sp *space) string { + if crate, ok := b.cargoOf(sp).Crate("src/lib.rs"); ok && crate.Dir == "" { + return crate.Name + } + return strings.ReplaceAll(sp.pkg.Name, "-", "_") +} + +// rustProvide returns the item of a crate of a unit of dependency code that +// a path of another crate names, as the struct SmallVec of the unit +// cargo:smallvec@1.13.2 for smallvec::SmallVec. +func rustProvide(b *builder, sp *space, module string, _ modules.Form) []provision { + parts := strings.Split(strings.TrimPrefix(module, "::"), "::") + if sp.pkg.Ecosystem != "cargo" || b.rustCrateName(sp) != parts[0] { + return nil + } + if key, ok := b.resolveQualified(langcode.Rust, joinPath(sp.unit, parts[1:])); ok { + return []provision{{sp: sp, module: key, via: "crate"}} + } + return []provision{{sp: sp}} +} + +// rustImportedModule returns the path of the package that a path of a file +// names. A path writes the name that the Cargo.toml of its crate gives a +// dependency, and the crate of the unit has the name of the package. +func rustImportedModule(b *builder, ix *fileindex.Index, module string) string { + sp := b.spaceOf(ix.Path) + first, rest, nested := strings.Cut(strings.TrimPrefix(module, "::"), "::") + pkg := b.cargoOf(sp).Rename(sp.inner(ix.Path), first) + switch { + case pkg == first: + return module + case nested: + return pkg + "::" + rest + } + return pkg +} + +// rustSurface adds each pub item of the crate of a unit and each pub +// method of its types, under the path that another crate writes for it. +func rustSurface(s *surfacer) { + b, sp := s.b, s.sp + if sp.pkg.Ecosystem != "cargo" { + return + } + for _, p := range b.paths { + ix := b.byPath[p] + if ix == nil || ix.Language != langcode.Rust || fileindex.PackageOf(p) != sp.unit { + continue + } + for _, def := range ix.Definitions { + segs := fileindex.Segments(def.Key) + if !def.Exported || len(segs) == 0 || len(segs) > 2 || len(segs) == 2 && def.Kind != fileindex.DefMethod { + continue + } + if module, names, ok := rustSymbol(b, sp, def.Key); ok { + s.add(langcode.Rust, module, modules.FormImport, names...) + } + } + } +} + +// rustSymbol returns the path of the module of a Rust definition of a unit +// as another crate writes it and the names of the definition, as smallvec +// and SmallVec, insert. A type of a private module that the root of the +// crate gives on with pub use gets the shortest path, as a user of the +// crate writes it. +func rustSymbol(b *builder, sp *space, def string) (string, []string, bool) { + module, ok := b.rustState().modules[fileindex.Unit(def)] + if !ok { + return "", nil, false + } + var segs []string + if _, rest, ok := strings.Cut(module, "::"); ok { + segs = strings.Split(rest, "::") + } + names := fileindex.Segments(def) + if len(names) == 0 { + return "", nil, false + } + top := fileindex.Join(fileindex.Unit(def), names[0]) + for i := range segs { + if key, ok := b.resolveQualified(langcode.Rust, joinPath(sp.unit, append(slices.Clone(segs[:i]), names[0]))); ok && key == top { + segs = segs[:i] + break + } + } + return joinPath(b.rustCrateName(sp), segs), names, true +} + func joinPath(module string, parts []string) string { return strings.Join(append([]string{module}, parts...), "::") } // resolveRust resolves a path that a file of the module writes to a // definition or a module of the project. +// +// A use binds the last name of its path, and the walk of the path can read +// that binding, as the first smallvec of use smallvec::smallvec. Rust +// resolves the first name of a use path against the crates and the modules, +// and never against the binding of the same use. So a path that the file +// resolves now gives no answer on a repeat, and the walk tries the next +// candidate. Without the check the walk does not end. func (b *builder) resolveRust(importer, written string) (string, bool) { - for _, p := range b.rustPaths(b.rustState().modules[importer], written) { + s := b.rustState() + k := [2]string{importer, written} + if s.resolving[k] { + return "", false + } + s.resolving[k] = true + defer delete(s.resolving, k) + for _, p := range b.rustPaths(s.modules[importer], written) { if key, ok := b.resolveQualified(langcode.Rust, p); ok { return key, true } @@ -183,8 +393,11 @@ func (b *builder) resolveRust(importer, written string) (string, bool) { } // rustResolver resolves a use, which names a module, a type or a function -// with its path. -type rustResolver struct{ b *builder } +// with its path, in one space. +type rustResolver struct { + b *builder + sp *space +} func (r rustResolver) Module(importer, spec string) (string, string, bool) { if key, ok := r.b.resolveRust(importer, spec); ok { @@ -204,13 +417,14 @@ func (r rustResolver) FirstParty(spec string) bool { case rustRoots[first]: return false } - return r.b.rustState().cargo.Has(first) || r.b.project.files.HasName(first) + return r.b.cargoOf(r.sp).Has(first) || r.sp.files.HasName(first) } // rustScopeFallback resolves a path that no use and no item of the module // binds: a path of the crate, as crate::store::save or store::save for a // child module, or else a path of another crate, as serde_json::to_string, -// whose external key the edge gets. A single name stays a record, because +// whose external key the edge gets. The crate is the origin of the key, so a +// unit of dependency code that gives the crate links it. A single name stays a record, because // a local closure can bind it. func (b *builder) rustScopeFallback(sc *scope, _, first string, rest []string, target string) []resolution { written := strings.Join(append([]string{first}, rest...), "::") @@ -218,7 +432,7 @@ func (b *builder) rustScopeFallback(sc *scope, _, first string, rest []string, t return []resolution{b.walkNamespace(key)} } if len(rest) > 0 { - return []resolution{b.externalResolution(target)} + return []resolution{b.importedWalk(sc.ix, target, first, modules.FormImport, rest)} } return nil } diff --git a/projectgraph/rust_test.go b/projectgraph/rust_test.go new file mode 100644 index 00000000..d50164d8 --- /dev/null +++ b/projectgraph/rust_test.go @@ -0,0 +1,105 @@ +package projectgraph + +import ( + "context" + "testing" + "testing/fstest" + + "github.com/safedep/code/fileindex" + "github.com/safedep/code/langcode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// rustUse is src/main.rs of the crate app with one use and a call of the +// name that the use binds. +func rustUse(module, alias string) *fileindex.Index { + const p = "src/main.rs" + ns := fileindex.Namespace(langcode.Rust, module) + return &fileindex.Index{ + Version: fileindex.Version, + Path: p, + Language: langcode.Rust, + Imports: []fileindex.Import{{Line: 1, Module: module, Alias: alias, Kind: fileindex.ImportStatic, Namespace: ns}}, + Definitions: []fileindex.Definition{ + def(fileindex.DefModule, p, 1), + def(fileindex.DefFunction, fileindex.Join(p, "main"), 2), + }, + Calls: []fileindex.Call{{Caller: fileindex.Join(p, "main"), Line: 2, Column: 13, Callee: alias, Targets: []string{ns}, Method: alias, Kind: fileindex.CallCall}}, + } +} + +func TestBuildRustUseOfItsOwnBinding(t *testing.T) { + root := fstest.MapFS{"Cargo.toml": {Data: []byte("[package]\nname = \"app\"\nversion = \"0.1.0\"\n")}} + tests := []struct { + name string + module string + alias string + edge string + }{ + {"use smallvec::smallvec", "smallvec::smallvec", "smallvec", "src/main.rs//main -> smallvec//smallvec direct rust.external"}, + {"use a::a", "a::a", "a", "src/main.rs//main -> a//a direct rust.external"}, + {"use a::a::a", "a::a::a", "a", "src/main.rs//main -> a//a//a direct rust.external"}, + // The module defines no a, so the program does not compile. The + // test checks that the link step ends. + {"use self::a as a", "self::a", "a", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g, err := Build(context.Background(), []*fileindex.Index{rustUse(tt.module, tt.alias)}, Options{Root: root}) + require.NoError(t, err) + if tt.edge != "" { + assert.Contains(t, edgeStrings(g), tt.edge) + } + }) + } +} + +// TestBuildRustWildcardUseOfTheClassInItsImpl links an impl of a trait for +// an enum whose method imports the variants of the enum with a wildcard, +// as use self::Component::* in impl Display for Component. The base +// Display can resolve through that wildcard, which leads back to the same +// class. +func TestBuildRustWildcardUseOfTheClassInItsImpl(t *testing.T) { + const p = "src/main.rs" + class := fileindex.Join(p, "Component") + ix := &fileindex.Index{ + Version: fileindex.Version, + Path: p, + Language: langcode.Rust, + Imports: []fileindex.Import{ + {Line: 1, Module: "std::fmt::Display", Alias: "Display", Kind: fileindex.ImportStatic, Namespace: "std//fmt//Display"}, + {Line: 9, Module: "self::Component", Wildcard: true, Kind: fileindex.ImportStatic, Namespace: "self//Component"}, + }, + Definitions: []fileindex.Definition{ + def(fileindex.DefModule, p, 1), + def(fileindex.DefClass, class, 7), + def(fileindex.DefMethod, fileindex.Join(class, "fmt"), 8), + }, + Classes: []fileindex.Class{{Key: class, Bases: []string{"Display"}, Methods: []string{fileindex.Join(class, "fmt")}}}, + } + root := fstest.MapFS{"Cargo.toml": {Data: []byte("[package]\nname = \"app\"\nversion = \"0.1.0\"\n")}} + g, err := Build(context.Background(), []*fileindex.Index{ix}, Options{Root: root}) + require.NoError(t, err) + assert.Equal(t, NodeClass, g.Nodes[class].Kind) +} + +func TestRustHelperProgram(t *testing.T) { + for _, tt := range []struct { + path string + want bool + }{ + {path: "build.rs", want: true}, + {path: "kit/build.rs", want: true}, + {path: "examples/demo.rs", want: true}, + {path: "kit/examples/demo/main.rs", want: true}, + {path: "benches/speed.rs", want: true}, + {path: "src/main.rs", want: false}, + {path: "src/bin/tool.rs", want: false}, + {path: "src/examples.rs", want: false}, + } { + t.Run(tt.path, func(t *testing.T) { + assert.Equal(t, tt.want, rustHelperProgram(tt.path)) + }) + } +} diff --git a/projectgraph/testdata/javascript/barrel-chain/graph.golden.json b/projectgraph/testdata/javascript/barrel-chain/graph.golden.json index 63337403..1a713c9f 100644 --- a/projectgraph/testdata/javascript/barrel-chain/graph.golden.json +++ b/projectgraph/testdata/javascript/barrel-chain/graph.golden.json @@ -174,7 +174,7 @@ "src/main.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 4, "nodes": 7, "edges": 6, diff --git a/projectgraph/testdata/javascript/barrel-chain/indexes.json b/projectgraph/testdata/javascript/barrel-chain/indexes.json index b5828e08..d1cffad8 100644 --- a/projectgraph/testdata/javascript/barrel-chain/indexes.json +++ b/projectgraph/testdata/javascript/barrel-chain/indexes.json @@ -1,22 +1,22 @@ [ { - "version": "11", "path": "src/main.js", "language": "javascript", + "version": "14", "path": "src/main.js", "language": "javascript", "imports": [{"line": 1, "module": "./api", "item": "parse", "alias": "parse", "kind": "static", "namespace": ".//api//parse"}], "definitions": [{"key": "src/main.js", "kind": "module", "name": "main", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "src/main.js", "line": 2, "column": 1, "callee": "parse", "targets": [".//api//parse"], "kind": "call"}] }, { - "version": "11", "path": "src/api/index.js", "language": "javascript", + "version": "14", "path": "src/api/index.js", "language": "javascript", "exports": [{"name": "parse", "target": "", "from": "../core", "item": "parse", "line": 1}], "definitions": [{"key": "src/api/index.js", "kind": "module", "name": "index", "start_line": 1, "end_line": 1, "exported": true}] }, { - "version": "11", "path": "src/core/index.js", "language": "javascript", + "version": "14", "path": "src/core/index.js", "language": "javascript", "exports": [{"name": "parse", "target": "", "from": "./parse", "item": "doParse", "line": 1}], "definitions": [{"key": "src/core/index.js", "kind": "module", "name": "index", "start_line": 1, "end_line": 1, "exported": true}] }, { - "version": "11", "path": "src/core/parse.js", "language": "javascript", + "version": "14", "path": "src/core/parse.js", "language": "javascript", "exports": [{"name": "doParse", "target": "src/core/parse.js//doParse", "line": 1}], "definitions": [ {"key": "src/core/parse.js", "kind": "module", "name": "parse", "start_line": 1, "end_line": 3, "exported": true}, diff --git a/projectgraph/testdata/javascript/commonjs-require/graph.golden.json b/projectgraph/testdata/javascript/commonjs-require/graph.golden.json index 9bcce014..b8f8c095 100644 --- a/projectgraph/testdata/javascript/commonjs-require/graph.golden.json +++ b/projectgraph/testdata/javascript/commonjs-require/graph.golden.json @@ -138,7 +138,7 @@ "main.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 5, "edges": 4, diff --git a/projectgraph/testdata/javascript/commonjs-require/indexes.json b/projectgraph/testdata/javascript/commonjs-require/indexes.json index b4b7e24f..cda79406 100644 --- a/projectgraph/testdata/javascript/commonjs-require/indexes.json +++ b/projectgraph/testdata/javascript/commonjs-require/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.js", "language": "javascript", + "version": "14", "path": "main.js", "language": "javascript", "imports": [ {"line": 1, "module": "./lib", "alias": "utils", "kind": "static", "namespace": ".//lib"}, {"line": 2, "module": "./helper", "item": "helper", "alias": "helper", "kind": "static", "namespace": ".//helper//helper"} @@ -12,7 +12,7 @@ ] }, { - "version": "11", "path": "lib/index.js", "language": "javascript", + "version": "14", "path": "lib/index.js", "language": "javascript", "exports": [{"name": "run", "target": "lib/index.js//run", "line": 3}], "definitions": [ {"key": "lib/index.js", "kind": "module", "name": "index", "start_line": 1, "end_line": 3, "exported": true}, @@ -20,7 +20,7 @@ ] }, { - "version": "11", "path": "helper.js", "language": "javascript", + "version": "14", "path": "helper.js", "language": "javascript", "exports": [{"name": "helper", "target": "helper.js//helper", "line": 3}], "definitions": [ {"key": "helper.js", "kind": "module", "name": "helper", "start_line": 1, "end_line": 3, "exported": true}, diff --git a/projectgraph/testdata/javascript/default-import/graph.golden.json b/projectgraph/testdata/javascript/default-import/graph.golden.json index 3ff37a2c..bc1b6927 100644 --- a/projectgraph/testdata/javascript/default-import/graph.golden.json +++ b/projectgraph/testdata/javascript/default-import/graph.golden.json @@ -138,7 +138,7 @@ "make.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 5, "edges": 4, diff --git a/projectgraph/testdata/javascript/default-import/indexes.json b/projectgraph/testdata/javascript/default-import/indexes.json index 8cc3dc0f..fcd832a1 100644 --- a/projectgraph/testdata/javascript/default-import/indexes.json +++ b/projectgraph/testdata/javascript/default-import/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.js", "language": "javascript", + "version": "14", "path": "main.js", "language": "javascript", "imports": [ {"line": 1, "module": "./log", "alias": "log", "kind": "static", "namespace": ".//log"}, {"line": 2, "module": "./make", "alias": "make", "kind": "static", "namespace": ".//make"} @@ -12,7 +12,7 @@ ] }, { - "version": "11", "path": "log.js", "language": "javascript", + "version": "14", "path": "log.js", "language": "javascript", "exports": [{"name": "default", "target": "log.js//log", "line": 1}], "definitions": [ {"key": "log.js", "kind": "module", "name": "log", "start_line": 1, "end_line": 1, "exported": true}, @@ -20,7 +20,7 @@ ] }, { - "version": "11", "path": "make.js", "language": "javascript", + "version": "14", "path": "make.js", "language": "javascript", "exports": [{"name": "default", "target": "make.js//make", "line": 1}], "definitions": [ {"key": "make.js", "kind": "module", "name": "make", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/javascript/esm-named-import/graph.golden.json b/projectgraph/testdata/javascript/esm-named-import/graph.golden.json index 3fdccbca..0595798c 100644 --- a/projectgraph/testdata/javascript/esm-named-import/graph.golden.json +++ b/projectgraph/testdata/javascript/esm-named-import/graph.golden.json @@ -166,7 +166,7 @@ "src/util.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 6, "edges": 5, diff --git a/projectgraph/testdata/javascript/esm-named-import/indexes.json b/projectgraph/testdata/javascript/esm-named-import/indexes.json index c1a44fbc..60c79a4b 100644 --- a/projectgraph/testdata/javascript/esm-named-import/indexes.json +++ b/projectgraph/testdata/javascript/esm-named-import/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "src/main.js", "language": "javascript", + "version": "14", "path": "src/main.js", "language": "javascript", "imports": [ {"line": 1, "module": "./parser.js", "item": "parse", "alias": "parse", "kind": "static", "namespace": ".//parser.js//parse"}, {"line": 2, "module": "./util", "item": "fmt", "alias": "fmt", "kind": "static", "namespace": ".//util//fmt"}, @@ -14,7 +14,7 @@ "unresolved": [{"caller": "src/main.js", "line": 5, "column": 1, "text": "import", "reason": "dynamic-import"}] }, { - "version": "11", "path": "src/parser.js", "language": "javascript", + "version": "14", "path": "src/parser.js", "language": "javascript", "imports": [{"line": 1, "module": "js-yaml", "alias": "yaml", "kind": "static", "namespace": "js-yaml"}], "exports": [{"name": "parse", "target": "src/parser.js//parse", "line": 2}], "definitions": [ @@ -24,7 +24,7 @@ "calls": [{"caller": "src/parser.js//parse", "line": 3, "column": 10, "callee": "yaml.load", "targets": ["js-yaml//load"], "receiver": "yaml", "method": "load", "kind": "call"}] }, { - "version": "11", "path": "src/util.js", "language": "javascript", + "version": "14", "path": "src/util.js", "language": "javascript", "exports": [{"name": "fmt", "target": "src/util.js//fmt", "line": 1}], "definitions": [ {"key": "src/util.js", "kind": "module", "name": "util", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/javascript/export-star/graph.golden.json b/projectgraph/testdata/javascript/export-star/graph.golden.json index 02e838f8..cacbbf58 100644 --- a/projectgraph/testdata/javascript/export-star/graph.golden.json +++ b/projectgraph/testdata/javascript/export-star/graph.golden.json @@ -276,7 +276,7 @@ "main.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 5, "nodes": 11, "edges": 10, diff --git a/projectgraph/testdata/javascript/export-star/indexes.json b/projectgraph/testdata/javascript/export-star/indexes.json index 6f5a0b20..0ede83ab 100644 --- a/projectgraph/testdata/javascript/export-star/indexes.json +++ b/projectgraph/testdata/javascript/export-star/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.js", "language": "javascript", + "version": "14", "path": "main.js", "language": "javascript", "imports": [ {"line": 1, "module": "./all", "item": "a", "alias": "a", "kind": "static", "namespace": ".//all//a"}, {"line": 1, "module": "./all", "item": "b", "alias": "b", "kind": "static", "namespace": ".//all//b"}, @@ -14,7 +14,7 @@ ] }, { - "version": "11", "path": "all.js", "language": "javascript", + "version": "14", "path": "all.js", "language": "javascript", "exports": [ {"name": "*", "target": "", "from": "./a", "item": "*", "line": 1}, {"name": "*", "target": "", "from": "./b", "item": "*", "line": 2}, @@ -23,7 +23,7 @@ "definitions": [{"key": "all.js", "kind": "module", "name": "all", "start_line": 1, "end_line": 3, "exported": true}] }, { - "version": "11", "path": "a.js", "language": "javascript", + "version": "14", "path": "a.js", "language": "javascript", "exports": [{"name": "a", "target": "a.js//a", "line": 1}], "definitions": [ {"key": "a.js", "kind": "module", "name": "a", "start_line": 1, "end_line": 1, "exported": true}, @@ -31,7 +31,7 @@ ] }, { - "version": "11", "path": "b.js", "language": "javascript", + "version": "14", "path": "b.js", "language": "javascript", "exports": [{"name": "b", "target": "b.js//b", "line": 1}], "definitions": [ {"key": "b.js", "kind": "module", "name": "b", "start_line": 1, "end_line": 1, "exported": true}, @@ -39,7 +39,7 @@ ] }, { - "version": "11", "path": "c.js", "language": "javascript", + "version": "14", "path": "c.js", "language": "javascript", "exports": [{"name": "c", "target": "c.js//c", "line": 1}], "definitions": [ {"key": "c.js", "kind": "module", "name": "c", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/javascript/exports-conditions/graph.golden.json b/projectgraph/testdata/javascript/exports-conditions/graph.golden.json index 6c487039..c48789e3 100644 --- a/projectgraph/testdata/javascript/exports-conditions/graph.golden.json +++ b/projectgraph/testdata/javascript/exports-conditions/graph.golden.json @@ -144,7 +144,7 @@ "lib/src/feature/y.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 4, "nodes": 7, "edges": 4, diff --git a/projectgraph/testdata/javascript/exports-conditions/indexes.json b/projectgraph/testdata/javascript/exports-conditions/indexes.json index a74f2959..c0aefd37 100644 --- a/projectgraph/testdata/javascript/exports-conditions/indexes.json +++ b/projectgraph/testdata/javascript/exports-conditions/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "app/main.mjs", "language": "javascript", + "version": "14", "path": "app/main.mjs", "language": "javascript", "imports": [ {"line": 1, "module": "@acme/lib", "item": "x", "alias": "x", "kind": "static", "namespace": "@acme//lib//x"}, {"line": 2, "module": "@acme/lib/feature/y", "item": "y", "alias": "y", "kind": "static", "namespace": "@acme//lib//feature//y//y"} @@ -12,7 +12,7 @@ ] }, { - "version": "11", "path": "lib/esm/index.mjs", "language": "javascript", + "version": "14", "path": "lib/esm/index.mjs", "language": "javascript", "exports": [{"name": "x", "target": "lib/esm/index.mjs//x", "line": 1}], "definitions": [ {"key": "lib/esm/index.mjs", "kind": "module", "name": "index", "start_line": 1, "end_line": 1, "exported": true}, @@ -20,7 +20,7 @@ ] }, { - "version": "11", "path": "lib/cjs/index.cjs", "language": "javascript", + "version": "14", "path": "lib/cjs/index.cjs", "language": "javascript", "exports": [{"name": "x", "target": "lib/cjs/index.cjs//x", "line": 1}], "definitions": [ {"key": "lib/cjs/index.cjs", "kind": "module", "name": "index", "start_line": 1, "end_line": 1, "exported": true}, @@ -28,7 +28,7 @@ ] }, { - "version": "11", "path": "lib/src/feature/y.js", "language": "javascript", + "version": "14", "path": "lib/src/feature/y.js", "language": "javascript", "exports": [{"name": "y", "target": "lib/src/feature/y.js//y", "line": 1}], "definitions": [ {"key": "lib/src/feature/y.js", "kind": "module", "name": "y", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/javascript/unknown-receiver/graph.golden.json b/projectgraph/testdata/javascript/unknown-receiver/graph.golden.json index fc701bf9..5fce3766 100644 --- a/projectgraph/testdata/javascript/unknown-receiver/graph.golden.json +++ b/projectgraph/testdata/javascript/unknown-receiver/graph.golden.json @@ -124,7 +124,7 @@ "main.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 5, "edges": 3, diff --git a/projectgraph/testdata/javascript/unknown-receiver/indexes.json b/projectgraph/testdata/javascript/unknown-receiver/indexes.json index 2015bb45..c4414fa2 100644 --- a/projectgraph/testdata/javascript/unknown-receiver/indexes.json +++ b/projectgraph/testdata/javascript/unknown-receiver/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "main.js", "language": "javascript", "imports": [ diff --git a/projectgraph/testdata/javascript/workspaces/graph.golden.json b/projectgraph/testdata/javascript/workspaces/graph.golden.json index 93924508..7af8a2f2 100644 --- a/projectgraph/testdata/javascript/workspaces/graph.golden.json +++ b/projectgraph/testdata/javascript/workspaces/graph.golden.json @@ -127,7 +127,7 @@ "packages/core/util.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 6, "edges": 5, diff --git a/projectgraph/testdata/javascript/workspaces/indexes.json b/projectgraph/testdata/javascript/workspaces/indexes.json index 3796b0a0..65319d4b 100644 --- a/projectgraph/testdata/javascript/workspaces/indexes.json +++ b/projectgraph/testdata/javascript/workspaces/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "packages/app/src/main.js", "language": "javascript", + "version": "14", "path": "packages/app/src/main.js", "language": "javascript", "imports": [ {"line": 1, "module": "@acme/core", "item": "start", "alias": "start", "kind": "static", "namespace": "@acme//core//start"}, {"line": 2, "module": "@acme/core/util", "item": "tidy", "alias": "tidy", "kind": "static", "namespace": "@acme//core//util//tidy"}, @@ -14,7 +14,7 @@ ] }, { - "version": "11", "path": "packages/core/lib/index.js", "language": "javascript", + "version": "14", "path": "packages/core/lib/index.js", "language": "javascript", "exports": [{"name": "start", "target": "packages/core/lib/index.js//start", "line": 1}], "definitions": [ {"key": "packages/core/lib/index.js", "kind": "module", "name": "index", "start_line": 1, "end_line": 1, "exported": true}, @@ -22,7 +22,7 @@ ] }, { - "version": "11", "path": "packages/core/util.js", "language": "javascript", + "version": "14", "path": "packages/core/util.js", "language": "javascript", "exports": [{"name": "tidy", "target": "packages/core/util.js//tidy", "line": 1}], "definitions": [ {"key": "packages/core/util.js", "kind": "module", "name": "util", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/python/all-hides/graph.golden.json b/projectgraph/testdata/python/all-hides/graph.golden.json index 307446f7..a5519cf5 100644 --- a/projectgraph/testdata/python/all-hides/graph.golden.json +++ b/projectgraph/testdata/python/all-hides/graph.golden.json @@ -174,7 +174,7 @@ "pkg/a.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 7, "edges": 6, diff --git a/projectgraph/testdata/python/all-hides/indexes.json b/projectgraph/testdata/python/all-hides/indexes.json index d16db4c3..bf36a029 100644 --- a/projectgraph/testdata/python/all-hides/indexes.json +++ b/projectgraph/testdata/python/all-hides/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [ {"line": 1, "module": "pkg", "item": "f", "alias": "f", "kind": "static", "namespace": "pkg//f"}, {"line": 1, "module": "pkg", "item": "g", "alias": "g", "kind": "static", "namespace": "pkg//g"} @@ -12,7 +12,7 @@ ] }, { - "version": "11", "path": "pkg/__init__.py", "language": "python", + "version": "14", "path": "pkg/__init__.py", "language": "python", "imports": [ {"line": 1, "module": ".a", "item": "f", "alias": "f", "kind": "static", "namespace": "//a//f"}, {"line": 1, "module": ".a", "item": "g", "alias": "g", "kind": "static", "namespace": "//a//g"} @@ -21,7 +21,7 @@ "definitions": [{"key": "pkg/__init__.py", "kind": "module", "name": "__init__", "start_line": 1, "end_line": 2, "exported": true}] }, { - "version": "11", "path": "pkg/a.py", "language": "python", + "version": "14", "path": "pkg/a.py", "language": "python", "definitions": [ {"key": "pkg/a.py", "kind": "module", "name": "a", "start_line": 1, "end_line": 4, "exported": true}, {"key": "pkg/a.py//f", "kind": "function", "name": "f", "start_line": 1, "end_line": 2, "exported": true}, diff --git a/projectgraph/testdata/python/assignment/graph.golden.json b/projectgraph/testdata/python/assignment/graph.golden.json index cca8c3f1..50c8c1d7 100644 --- a/projectgraph/testdata/python/assignment/graph.golden.json +++ b/projectgraph/testdata/python/assignment/graph.golden.json @@ -70,7 +70,7 @@ "main.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 3, "edges": 2, diff --git a/projectgraph/testdata/python/assignment/indexes.json b/projectgraph/testdata/python/assignment/indexes.json index fb85ac4c..4a129982 100644 --- a/projectgraph/testdata/python/assignment/indexes.json +++ b/projectgraph/testdata/python/assignment/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "yaml", "alias": "yaml", "kind": "static", "namespace": "yaml"}], "definitions": [ {"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 7, "exported": true}, diff --git a/projectgraph/testdata/python/class-body/graph.golden.json b/projectgraph/testdata/python/class-body/graph.golden.json index 7befc242..2d7290cb 100644 --- a/projectgraph/testdata/python/class-body/graph.golden.json +++ b/projectgraph/testdata/python/class-body/graph.golden.json @@ -224,7 +224,7 @@ "config.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 12, "edges": 7, diff --git a/projectgraph/testdata/python/class-body/indexes.json b/projectgraph/testdata/python/class-body/indexes.json index 03b1db43..aa75873e 100644 --- a/projectgraph/testdata/python/class-body/indexes.json +++ b/projectgraph/testdata/python/class-body/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "config.py", "language": "python", "imports": [ diff --git a/projectgraph/testdata/python/decorators/graph.golden.json b/projectgraph/testdata/python/decorators/graph.golden.json index 6265dbc9..69b4630e 100644 --- a/projectgraph/testdata/python/decorators/graph.golden.json +++ b/projectgraph/testdata/python/decorators/graph.golden.json @@ -194,7 +194,7 @@ "app.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 9, "edges": 5, diff --git a/projectgraph/testdata/python/decorators/indexes.json b/projectgraph/testdata/python/decorators/indexes.json index 6733d0da..b5f50d62 100644 --- a/projectgraph/testdata/python/decorators/indexes.json +++ b/projectgraph/testdata/python/decorators/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "app.py", "language": "python", "imports": [ diff --git a/projectgraph/testdata/python/external/graph.golden.json b/projectgraph/testdata/python/external/graph.golden.json index dec84c25..8e6116a8 100644 --- a/projectgraph/testdata/python/external/graph.golden.json +++ b/projectgraph/testdata/python/external/graph.golden.json @@ -108,7 +108,7 @@ "main.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 5, "edges": 3, diff --git a/projectgraph/testdata/python/external/indexes.json b/projectgraph/testdata/python/external/indexes.json index a096c376..8abec365 100644 --- a/projectgraph/testdata/python/external/indexes.json +++ b/projectgraph/testdata/python/external/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [ {"line": 1, "module": "requests", "alias": "requests", "kind": "static", "namespace": "requests"}, {"line": 2, "module": "yaml", "item": "load", "alias": "yl", "kind": "static", "namespace": "yaml//load"}, diff --git a/projectgraph/testdata/python/local-receiver/graph.golden.json b/projectgraph/testdata/python/local-receiver/graph.golden.json index b7380485..3a333db8 100644 --- a/projectgraph/testdata/python/local-receiver/graph.golden.json +++ b/projectgraph/testdata/python/local-receiver/graph.golden.json @@ -145,7 +145,7 @@ "main.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 5, "edges": 3, diff --git a/projectgraph/testdata/python/local-receiver/indexes.json b/projectgraph/testdata/python/local-receiver/indexes.json index b93898f0..09ba712c 100644 --- a/projectgraph/testdata/python/local-receiver/indexes.json +++ b/projectgraph/testdata/python/local-receiver/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "main.py", "language": "python", "imports": [ diff --git a/projectgraph/testdata/python/member-call/graph.golden.json b/projectgraph/testdata/python/member-call/graph.golden.json index 268a8edf..f2d92a37 100644 --- a/projectgraph/testdata/python/member-call/graph.golden.json +++ b/projectgraph/testdata/python/member-call/graph.golden.json @@ -258,7 +258,7 @@ "parser.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 9, "edges": 10, diff --git a/projectgraph/testdata/python/member-call/indexes.json b/projectgraph/testdata/python/member-call/indexes.json index e7eed657..0d0fd90f 100644 --- a/projectgraph/testdata/python/member-call/indexes.json +++ b/projectgraph/testdata/python/member-call/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "parser", "item": "Parser", "alias": "Parser", "kind": "static", "namespace": "parser//Parser"}], "definitions": [{"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 4, "exported": true}], "calls": [ @@ -10,7 +10,7 @@ ] }, { - "version": "11", "path": "parser.py", "language": "python", + "version": "14", "path": "parser.py", "language": "python", "imports": [{"line": 1, "module": "base", "item": "Base", "alias": "Base", "kind": "static", "namespace": "base//Base"}], "definitions": [ {"key": "parser.py", "kind": "module", "name": "parser", "start_line": 1, "end_line": 7, "exported": true}, @@ -22,7 +22,7 @@ "calls": [{"caller": "parser.py//Parser//parse", "line": 6, "column": 9, "callee": "self.helper", "targets": ["parser.py//Parser//helper"], "receiver": "self", "method": "helper", "kind": "call"}] }, { - "version": "11", "path": "base.py", "language": "python", + "version": "14", "path": "base.py", "language": "python", "imports": [{"line": 1, "module": "io", "alias": "io", "kind": "static", "namespace": "io"}], "definitions": [ {"key": "base.py", "kind": "module", "name": "base", "start_line": 1, "end_line": 4, "exported": true}, diff --git a/projectgraph/testdata/python/missing-file/graph.golden.json b/projectgraph/testdata/python/missing-file/graph.golden.json index 82613f9a..249c0096 100644 --- a/projectgraph/testdata/python/missing-file/graph.golden.json +++ b/projectgraph/testdata/python/missing-file/graph.golden.json @@ -55,7 +55,7 @@ "main.py": "cached" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 2, "edges": 2, diff --git a/projectgraph/testdata/python/missing-file/indexes.json b/projectgraph/testdata/python/missing-file/indexes.json index 1f481508..b42f5396 100644 --- a/projectgraph/testdata/python/missing-file/indexes.json +++ b/projectgraph/testdata/python/missing-file/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "handlers", "item": "parse", "alias": "parse", "kind": "static", "namespace": "handlers//parse"}], "definitions": [{"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "main.py", "line": 2, "column": 1, "callee": "parse", "targets": ["handlers//parse"], "kind": "call"}] diff --git a/projectgraph/testdata/python/package-init/graph.golden.json b/projectgraph/testdata/python/package-init/graph.golden.json index f0351b4b..3ae14b1f 100644 --- a/projectgraph/testdata/python/package-init/graph.golden.json +++ b/projectgraph/testdata/python/package-init/graph.golden.json @@ -144,7 +144,7 @@ "pkg/loader.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 6, "edges": 5, diff --git a/projectgraph/testdata/python/package-init/indexes.json b/projectgraph/testdata/python/package-init/indexes.json index e57f197c..6e275227 100644 --- a/projectgraph/testdata/python/package-init/indexes.json +++ b/projectgraph/testdata/python/package-init/indexes.json @@ -1,17 +1,17 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "pkg", "item": "load", "alias": "load", "kind": "static", "namespace": "pkg//load"}], "definitions": [{"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "main.py", "line": 2, "column": 1, "callee": "load", "targets": ["pkg//load"], "kind": "call"}] }, { - "version": "11", "path": "pkg/__init__.py", "language": "python", + "version": "14", "path": "pkg/__init__.py", "language": "python", "imports": [{"line": 1, "module": ".loader", "item": "load", "alias": "load", "kind": "static", "namespace": "//loader//load"}], "definitions": [{"key": "pkg/__init__.py", "kind": "module", "name": "__init__", "start_line": 1, "end_line": 1, "exported": true}] }, { - "version": "11", "path": "pkg/loader.py", "language": "python", + "version": "14", "path": "pkg/loader.py", "language": "python", "imports": [{"line": 1, "module": "yaml", "alias": "yaml", "kind": "static", "namespace": "yaml"}], "definitions": [ {"key": "pkg/loader.py", "kind": "module", "name": "loader", "start_line": 1, "end_line": 4, "exported": true}, diff --git a/projectgraph/testdata/python/reexport-cycle/graph.golden.json b/projectgraph/testdata/python/reexport-cycle/graph.golden.json index da43f978..c9b069c0 100644 --- a/projectgraph/testdata/python/reexport-cycle/graph.golden.json +++ b/projectgraph/testdata/python/reexport-cycle/graph.golden.json @@ -105,7 +105,7 @@ "main.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 3, "edges": 3, diff --git a/projectgraph/testdata/python/reexport-cycle/indexes.json b/projectgraph/testdata/python/reexport-cycle/indexes.json index c73126f5..d5dc08b8 100644 --- a/projectgraph/testdata/python/reexport-cycle/indexes.json +++ b/projectgraph/testdata/python/reexport-cycle/indexes.json @@ -1,17 +1,17 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "a", "item": "x", "alias": "x", "kind": "static", "namespace": "a//x"}], "definitions": [{"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "main.py", "line": 2, "column": 1, "callee": "x", "targets": ["a//x"], "kind": "call"}] }, { - "version": "11", "path": "a/__init__.py", "language": "python", + "version": "14", "path": "a/__init__.py", "language": "python", "imports": [{"line": 1, "module": "b", "item": "x", "alias": "x", "kind": "static", "namespace": "b//x"}], "definitions": [{"key": "a/__init__.py", "kind": "module", "name": "__init__", "start_line": 1, "end_line": 1, "exported": true}] }, { - "version": "11", "path": "b/__init__.py", "language": "python", + "version": "14", "path": "b/__init__.py", "language": "python", "imports": [{"line": 1, "module": "a", "item": "x", "alias": "x", "kind": "static", "namespace": "a//x"}], "definitions": [{"key": "b/__init__.py", "kind": "module", "name": "__init__", "start_line": 1, "end_line": 1, "exported": true}] } diff --git a/projectgraph/testdata/python/relative-imports/graph.golden.json b/projectgraph/testdata/python/relative-imports/graph.golden.json index 0802264e..fd51479a 100644 --- a/projectgraph/testdata/python/relative-imports/graph.golden.json +++ b/projectgraph/testdata/python/relative-imports/graph.golden.json @@ -154,7 +154,7 @@ "pkg/sub/b.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 6, "edges": 4, diff --git a/projectgraph/testdata/python/relative-imports/indexes.json b/projectgraph/testdata/python/relative-imports/indexes.json index 9ee7e1e0..db008b91 100644 --- a/projectgraph/testdata/python/relative-imports/indexes.json +++ b/projectgraph/testdata/python/relative-imports/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "pkg/sub/a.py", "language": "python", + "version": "14", "path": "pkg/sub/a.py", "language": "python", "imports": [ {"line": 1, "module": ".b", "item": "f", "alias": "f", "kind": "static", "namespace": "//b//f"}, {"line": 2, "module": "..c", "item": "g", "alias": "g", "kind": "static", "namespace": "////c//g"} @@ -15,14 +15,14 @@ ] }, { - "version": "11", "path": "pkg/sub/b.py", "language": "python", + "version": "14", "path": "pkg/sub/b.py", "language": "python", "definitions": [ {"key": "pkg/sub/b.py", "kind": "module", "name": "b", "start_line": 1, "end_line": 2, "exported": true}, {"key": "pkg/sub/b.py//f", "kind": "function", "name": "f", "start_line": 1, "end_line": 2, "exported": true} ] }, { - "version": "11", "path": "pkg/c.py", "language": "python", + "version": "14", "path": "pkg/c.py", "language": "python", "definitions": [ {"key": "pkg/c.py", "kind": "module", "name": "c", "start_line": 1, "end_line": 2, "exported": true}, {"key": "pkg/c.py//g", "kind": "function", "name": "g", "start_line": 1, "end_line": 2, "exported": true} diff --git a/projectgraph/testdata/python/sibling-import/graph.golden.json b/projectgraph/testdata/python/sibling-import/graph.golden.json index 3d1a15fa..cebc8e7d 100644 --- a/projectgraph/testdata/python/sibling-import/graph.golden.json +++ b/projectgraph/testdata/python/sibling-import/graph.golden.json @@ -98,7 +98,7 @@ "main.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 4, "edges": 3, diff --git a/projectgraph/testdata/python/sibling-import/indexes.json b/projectgraph/testdata/python/sibling-import/indexes.json index 7aa4de6f..b3c87e12 100644 --- a/projectgraph/testdata/python/sibling-import/indexes.json +++ b/projectgraph/testdata/python/sibling-import/indexes.json @@ -1,12 +1,12 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "handlers", "item": "parse", "alias": "parse", "kind": "static", "namespace": "handlers//parse"}], "definitions": [{"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 3, "exported": true}], "calls": [{"caller": "main.py", "line": 3, "column": 1, "callee": "parse", "targets": ["handlers//parse"], "kind": "call"}] }, { - "version": "11", "path": "handlers.py", "language": "python", + "version": "14", "path": "handlers.py", "language": "python", "imports": [{"line": 1, "module": "yaml", "alias": "yaml", "kind": "static", "namespace": "yaml"}], "definitions": [ {"key": "handlers.py", "kind": "module", "name": "handlers", "start_line": 1, "end_line": 4, "exported": true}, diff --git a/projectgraph/testdata/python/src-layout/graph.golden.json b/projectgraph/testdata/python/src-layout/graph.golden.json index 140f4fa8..9f2d1eb9 100644 --- a/projectgraph/testdata/python/src-layout/graph.golden.json +++ b/projectgraph/testdata/python/src-layout/graph.golden.json @@ -99,7 +99,7 @@ "services/api/src/api/core.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 4, "edges": 2, diff --git a/projectgraph/testdata/python/src-layout/indexes.json b/projectgraph/testdata/python/src-layout/indexes.json index b659b88c..8dab6e9b 100644 --- a/projectgraph/testdata/python/src-layout/indexes.json +++ b/projectgraph/testdata/python/src-layout/indexes.json @@ -1,16 +1,16 @@ [ { - "version": "11", "path": "services/api/src/api/__init__.py", "language": "python", + "version": "14", "path": "services/api/src/api/__init__.py", "language": "python", "definitions": [{"key": "services/api/src/api/__init__.py", "kind": "module", "name": "__init__", "start_line": 1, "end_line": 1, "exported": true}] }, { - "version": "11", "path": "services/api/src/api/cli.py", "language": "python", + "version": "14", "path": "services/api/src/api/cli.py", "language": "python", "imports": [{"line": 1, "module": "api.core", "item": "run", "alias": "run", "kind": "static", "namespace": "api//core//run"}], "definitions": [{"key": "services/api/src/api/cli.py", "kind": "module", "name": "cli", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "services/api/src/api/cli.py", "line": 2, "column": 1, "callee": "run", "targets": ["api//core//run"], "kind": "call"}] }, { - "version": "11", "path": "services/api/src/api/core.py", "language": "python", + "version": "14", "path": "services/api/src/api/core.py", "language": "python", "definitions": [ {"key": "services/api/src/api/core.py", "kind": "module", "name": "core", "start_line": 1, "end_line": 2, "exported": true}, {"key": "services/api/src/api/core.py//run", "kind": "function", "name": "run", "start_line": 1, "end_line": 2, "exported": true} diff --git a/projectgraph/testdata/python/sys-path/graph.golden.json b/projectgraph/testdata/python/sys-path/graph.golden.json index 6c7b8e99..2ed50c1b 100644 --- a/projectgraph/testdata/python/sys-path/graph.golden.json +++ b/projectgraph/testdata/python/sys-path/graph.golden.json @@ -82,7 +82,7 @@ "lib/shared.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 3, "edges": 2, diff --git a/projectgraph/testdata/python/sys-path/indexes.json b/projectgraph/testdata/python/sys-path/indexes.json index fc4c5952..372cc0fb 100644 --- a/projectgraph/testdata/python/sys-path/indexes.json +++ b/projectgraph/testdata/python/sys-path/indexes.json @@ -1,12 +1,12 @@ [ { - "version": "11", "path": "app/main.py", "language": "python", + "version": "14", "path": "app/main.py", "language": "python", "imports": [{"line": 1, "module": "shared", "alias": "shared", "kind": "static", "namespace": "shared"}], "definitions": [{"key": "app/main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "app/main.py", "line": 2, "column": 1, "callee": "shared.go", "targets": ["shared//go"], "receiver": "shared", "method": "go", "kind": "call"}] }, { - "version": "11", "path": "lib/shared.py", "language": "python", + "version": "14", "path": "lib/shared.py", "language": "python", "definitions": [ {"key": "lib/shared.py", "kind": "module", "name": "shared", "start_line": 1, "end_line": 2, "exported": true}, {"key": "lib/shared.py//go", "kind": "function", "name": "go", "start_line": 1, "end_line": 2, "exported": true} diff --git a/projectgraph/testdata/python/wildcard/graph.golden.json b/projectgraph/testdata/python/wildcard/graph.golden.json index 5336f3ba..db310758 100644 --- a/projectgraph/testdata/python/wildcard/graph.golden.json +++ b/projectgraph/testdata/python/wildcard/graph.golden.json @@ -112,7 +112,7 @@ "util.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 4, "edges": 3, diff --git a/projectgraph/testdata/python/wildcard/indexes.json b/projectgraph/testdata/python/wildcard/indexes.json index cab119cf..8c56f11b 100644 --- a/projectgraph/testdata/python/wildcard/indexes.json +++ b/projectgraph/testdata/python/wildcard/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "main.py", "language": "python", + "version": "14", "path": "main.py", "language": "python", "imports": [{"line": 1, "module": "util", "wildcard": true, "kind": "static", "namespace": "util"}], "definitions": [{"key": "main.py", "kind": "module", "name": "main", "start_line": 1, "end_line": 3, "exported": true}], "calls": [ @@ -9,7 +9,7 @@ ] }, { - "version": "11", "path": "util.py", "language": "python", + "version": "14", "path": "util.py", "language": "python", "definitions": [ {"key": "util.py", "kind": "module", "name": "util", "start_line": 1, "end_line": 4, "exported": true}, {"key": "util.py//helper", "kind": "function", "name": "helper", "start_line": 1, "end_line": 2, "exported": true}, diff --git a/projectgraph/testdata/roots/extra-rules/graph.golden.json b/projectgraph/testdata/roots/extra-rules/graph.golden.json index 18a50927..da675a69 100644 --- a/projectgraph/testdata/roots/extra-rules/graph.golden.json +++ b/projectgraph/testdata/roots/extra-rules/graph.golden.json @@ -105,7 +105,7 @@ "views.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 7, "edges": 0, diff --git a/projectgraph/testdata/roots/extra-rules/indexes.json b/projectgraph/testdata/roots/extra-rules/indexes.json index b69e638a..12b245f9 100644 --- a/projectgraph/testdata/roots/extra-rules/indexes.json +++ b/projectgraph/testdata/roots/extra-rules/indexes.json @@ -1,10 +1,10 @@ [ { - "version": "11", "path": "app/users/route.ts", "language": "typescript", + "version": "14", "path": "app/users/route.ts", "language": "typescript", "definitions": [{"key": "app/users/route.ts", "kind": "module", "name": "route", "start_line": 1, "end_line": 1, "exported": true}] }, { - "version": "11", "path": "src/handler.js", "language": "javascript", + "version": "14", "path": "src/handler.js", "language": "javascript", "definitions": [ {"key": "src/handler.js", "kind": "module", "name": "handler", "start_line": 1, "end_line": 4, "exported": true}, {"key": "src/handler.js//Handler", "kind": "class", "name": "Handler", "start_line": 1, "end_line": 4, "exported": true}, @@ -13,7 +13,7 @@ ] }, { - "version": "11", "path": "views.py", "language": "python", + "version": "14", "path": "views.py", "language": "python", "definitions": [ {"key": "views.py", "kind": "module", "name": "views", "start_line": 1, "end_line": 3, "exported": true}, {"key": "views.py//index", "kind": "function", "name": "index", "start_line": 2, "end_line": 3, "exported": true, "decorators": ["@app.route"]} diff --git a/projectgraph/testdata/roots/library-surface/graph.golden.json b/projectgraph/testdata/roots/library-surface/graph.golden.json index 6da1147a..5c8b68da 100644 --- a/projectgraph/testdata/roots/library-surface/graph.golden.json +++ b/projectgraph/testdata/roots/library-surface/graph.golden.json @@ -82,7 +82,7 @@ "lib/test_api.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 5, "edges": 0, diff --git a/projectgraph/testdata/roots/library-surface/indexes.json b/projectgraph/testdata/roots/library-surface/indexes.json index ee7a7d8e..a89db48a 100644 --- a/projectgraph/testdata/roots/library-surface/indexes.json +++ b/projectgraph/testdata/roots/library-surface/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "lib/api.py", "language": "python", + "version": "14", "path": "lib/api.py", "language": "python", "definitions": [ {"key": "lib/api.py", "kind": "module", "name": "api", "start_line": 1, "end_line": 4, "exported": true}, {"key": "lib/api.py//public", "kind": "function", "name": "public", "start_line": 1, "end_line": 2, "exported": true}, @@ -8,7 +8,7 @@ ] }, { - "version": "11", "path": "lib/test_api.py", "language": "python", + "version": "14", "path": "lib/test_api.py", "language": "python", "definitions": [ {"key": "lib/test_api.py", "kind": "module", "name": "test_api", "start_line": 1, "end_line": 2, "exported": true}, {"key": "lib/test_api.py//test_public", "kind": "function", "name": "test_public", "start_line": 1, "end_line": 2, "exported": true} diff --git a/projectgraph/testdata/roots/main-guard/graph.golden.json b/projectgraph/testdata/roots/main-guard/graph.golden.json index 5d43b03c..d5b4b552 100644 --- a/projectgraph/testdata/roots/main-guard/graph.golden.json +++ b/projectgraph/testdata/roots/main-guard/graph.golden.json @@ -60,7 +60,7 @@ "lib.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 4, "edges": 1, diff --git a/projectgraph/testdata/roots/main-guard/indexes.json b/projectgraph/testdata/roots/main-guard/indexes.json index fdf6b04c..890a4e4b 100644 --- a/projectgraph/testdata/roots/main-guard/indexes.json +++ b/projectgraph/testdata/roots/main-guard/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "app.py", "language": "python", "main_guard": true, + "version": "14", "path": "app.py", "language": "python", "main_guard": true, "definitions": [ {"key": "app.py", "kind": "module", "name": "app", "start_line": 1, "end_line": 5, "exported": true}, {"key": "app.py//run", "kind": "function", "name": "run", "start_line": 1, "end_line": 2, "exported": true} @@ -8,7 +8,7 @@ "calls": [{"caller": "app.py", "line": 5, "column": 5, "callee": "run", "targets": ["app.py//run"], "kind": "call"}] }, { - "version": "11", "path": "lib.py", "language": "python", + "version": "14", "path": "lib.py", "language": "python", "definitions": [ {"key": "lib.py", "kind": "module", "name": "lib", "start_line": 1, "end_line": 2, "exported": true}, {"key": "lib.py//lib", "kind": "function", "name": "lib", "start_line": 1, "end_line": 2, "exported": true} diff --git a/projectgraph/testdata/roots/package-json-bin/graph.golden.json b/projectgraph/testdata/roots/package-json-bin/graph.golden.json index 27bec44c..11a8e36f 100644 --- a/projectgraph/testdata/roots/package-json-bin/graph.golden.json +++ b/projectgraph/testdata/roots/package-json-bin/graph.golden.json @@ -64,7 +64,7 @@ "lib/run.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 3, "edges": 2, diff --git a/projectgraph/testdata/roots/package-json-bin/indexes.json b/projectgraph/testdata/roots/package-json-bin/indexes.json index 47af7d1c..ef2b0a99 100644 --- a/projectgraph/testdata/roots/package-json-bin/indexes.json +++ b/projectgraph/testdata/roots/package-json-bin/indexes.json @@ -1,12 +1,12 @@ [ { - "version": "11", "path": "bin/cli.js", "language": "javascript", + "version": "14", "path": "bin/cli.js", "language": "javascript", "imports": [{"line": 1, "module": "../lib/run", "item": "run", "alias": "run", "kind": "static", "namespace": "..//lib//run//run"}], "definitions": [{"key": "bin/cli.js", "kind": "module", "name": "cli", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "bin/cli.js", "line": 2, "column": 1, "callee": "run", "targets": ["..//lib//run//run"], "kind": "call"}] }, { - "version": "11", "path": "lib/run.js", "language": "javascript", + "version": "14", "path": "lib/run.js", "language": "javascript", "exports": [{"name": "run", "target": "lib/run.js//run", "line": 1}], "definitions": [ {"key": "lib/run.js", "kind": "module", "name": "run", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/roots/package-json-main/graph.golden.json b/projectgraph/testdata/roots/package-json-main/graph.golden.json index 530a1404..dc63358f 100644 --- a/projectgraph/testdata/roots/package-json-main/graph.golden.json +++ b/projectgraph/testdata/roots/package-json-main/graph.golden.json @@ -154,7 +154,7 @@ "parser.js": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 4, "nodes": 9, "edges": 3, diff --git a/projectgraph/testdata/roots/package-json-main/indexes.json b/projectgraph/testdata/roots/package-json-main/indexes.json index 06915977..56e5e660 100644 --- a/projectgraph/testdata/roots/package-json-main/indexes.json +++ b/projectgraph/testdata/roots/package-json-main/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "cli.js", "language": "javascript", "exports": [ @@ -30,7 +30,7 @@ ] }, { - "version": "11", + "version": "14", "path": "fmt.js", "language": "javascript", "exports": [ @@ -60,7 +60,7 @@ ] }, { - "version": "11", + "version": "14", "path": "index.js", "language": "javascript", "exports": [ @@ -104,7 +104,7 @@ ] }, { - "version": "11", + "version": "14", "path": "parser.js", "language": "javascript", "exports": [ diff --git a/projectgraph/testdata/roots/procfile-dockerfile/graph.golden.json b/projectgraph/testdata/roots/procfile-dockerfile/graph.golden.json index 94fa85fc..4b86f86c 100644 --- a/projectgraph/testdata/roots/procfile-dockerfile/graph.golden.json +++ b/projectgraph/testdata/roots/procfile-dockerfile/graph.golden.json @@ -108,7 +108,7 @@ "worker.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 6, "nodes": 7, "edges": 0, diff --git a/projectgraph/testdata/roots/procfile-dockerfile/indexes.json b/projectgraph/testdata/roots/procfile-dockerfile/indexes.json index 6fd684cd..3955f2b6 100644 --- a/projectgraph/testdata/roots/procfile-dockerfile/indexes.json +++ b/projectgraph/testdata/roots/procfile-dockerfile/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "app/wsgi.py", "language": "python", "definitions": [ @@ -23,7 +23,7 @@ ] }, { - "version": "11", + "version": "14", "path": "app/cli.py", "language": "python", "definitions": [ @@ -38,7 +38,7 @@ ] }, { - "version": "11", + "version": "14", "path": "app/__init__.py", "language": "python", "definitions": [ @@ -53,7 +53,7 @@ ] }, { - "version": "11", + "version": "14", "path": "worker.py", "language": "python", "definitions": [ @@ -68,7 +68,7 @@ ] }, { - "version": "11", + "version": "14", "path": "ignored.py", "language": "python", "definitions": [ @@ -83,7 +83,7 @@ ] }, { - "version": "11", + "version": "14", "path": "web/server.js", "language": "javascript", "definitions": [ diff --git a/projectgraph/testdata/roots/pyproject-scripts/graph.golden.json b/projectgraph/testdata/roots/pyproject-scripts/graph.golden.json index 32ca0b67..62d83799 100644 --- a/projectgraph/testdata/roots/pyproject-scripts/graph.golden.json +++ b/projectgraph/testdata/roots/pyproject-scripts/graph.golden.json @@ -146,7 +146,7 @@ "src/app/tool.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 5, "nodes": 10, "edges": 0, diff --git a/projectgraph/testdata/roots/pyproject-scripts/indexes.json b/projectgraph/testdata/roots/pyproject-scripts/indexes.json index 39247fca..38244e1b 100644 --- a/projectgraph/testdata/roots/pyproject-scripts/indexes.json +++ b/projectgraph/testdata/roots/pyproject-scripts/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "src/app/cli.py", "language": "python", "definitions": [ @@ -23,7 +23,7 @@ ] }, { - "version": "11", + "version": "14", "path": "src/app/tool.py", "language": "python", "definitions": [ @@ -46,7 +46,7 @@ ] }, { - "version": "11", + "version": "14", "path": "src/app/plugin.py", "language": "python", "definitions": [ @@ -78,7 +78,7 @@ ] }, { - "version": "11", + "version": "14", "path": "src/app/legacy.py", "language": "python", "definitions": [ @@ -93,7 +93,7 @@ ] }, { - "version": "11", + "version": "14", "path": "src/app/ignored.py", "language": "python", "definitions": [ diff --git a/projectgraph/testdata/roots/setup-py/graph.golden.json b/projectgraph/testdata/roots/setup-py/graph.golden.json index 823d259a..ab3a569a 100644 --- a/projectgraph/testdata/roots/setup-py/graph.golden.json +++ b/projectgraph/testdata/roots/setup-py/graph.golden.json @@ -74,7 +74,7 @@ "tool/main.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 4, "edges": 0, diff --git a/projectgraph/testdata/roots/setup-py/indexes.json b/projectgraph/testdata/roots/setup-py/indexes.json index aa06c36a..5a20a349 100644 --- a/projectgraph/testdata/roots/setup-py/indexes.json +++ b/projectgraph/testdata/roots/setup-py/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", + "version": "14", "path": "tool/main.py", "language": "python", "definitions": [ @@ -23,7 +23,7 @@ ] }, { - "version": "11", + "version": "14", "path": "tool/admin.py", "language": "python", "definitions": [ diff --git a/projectgraph/testdata/roots/test-file/graph.golden.json b/projectgraph/testdata/roots/test-file/graph.golden.json index 99a21c73..97089938 100644 --- a/projectgraph/testdata/roots/test-file/graph.golden.json +++ b/projectgraph/testdata/roots/test-file/graph.golden.json @@ -68,7 +68,7 @@ "tests/test_app.py": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 1, "nodes": 4, "edges": 0, diff --git a/projectgraph/testdata/roots/test-file/indexes.json b/projectgraph/testdata/roots/test-file/indexes.json index f2471837..4059f41f 100644 --- a/projectgraph/testdata/roots/test-file/indexes.json +++ b/projectgraph/testdata/roots/test-file/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "tests/test_app.py", "language": "python", + "version": "14", "path": "tests/test_app.py", "language": "python", "definitions": [ {"key": "tests/test_app.py", "kind": "module", "name": "test_app", "start_line": 1, "end_line": 6, "exported": true}, {"key": "tests/test_app.py//TestApp", "kind": "class", "name": "TestApp", "start_line": 1, "end_line": 4, "exported": true}, diff --git a/projectgraph/testdata/typescript/js-specifier/graph.golden.json b/projectgraph/testdata/typescript/js-specifier/graph.golden.json index c8062ccd..b1ffb28d 100644 --- a/projectgraph/testdata/typescript/js-specifier/graph.golden.json +++ b/projectgraph/testdata/typescript/js-specifier/graph.golden.json @@ -82,7 +82,7 @@ "src/util.ts": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 2, "nodes": 3, "edges": 2, diff --git a/projectgraph/testdata/typescript/js-specifier/indexes.json b/projectgraph/testdata/typescript/js-specifier/indexes.json index 8ce8657b..680e8985 100644 --- a/projectgraph/testdata/typescript/js-specifier/indexes.json +++ b/projectgraph/testdata/typescript/js-specifier/indexes.json @@ -1,12 +1,12 @@ [ { - "version": "11", "path": "src/main.ts", "language": "typescript", + "version": "14", "path": "src/main.ts", "language": "typescript", "imports": [{"line": 1, "module": "./util.js", "item": "f", "alias": "f", "kind": "static", "namespace": ".//util.js//f"}], "definitions": [{"key": "src/main.ts", "kind": "module", "name": "main", "start_line": 1, "end_line": 2, "exported": true}], "calls": [{"caller": "src/main.ts", "line": 2, "column": 1, "callee": "f", "targets": [".//util.js//f"], "kind": "call"}] }, { - "version": "11", "path": "src/util.ts", "language": "typescript", + "version": "14", "path": "src/util.ts", "language": "typescript", "exports": [{"name": "f", "target": "src/util.ts//f", "line": 1}], "definitions": [ {"key": "src/util.ts", "kind": "module", "name": "util", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/typescript/tsconfig-paths/graph.golden.json b/projectgraph/testdata/typescript/tsconfig-paths/graph.golden.json index 631dd1b2..81b607c7 100644 --- a/projectgraph/testdata/typescript/tsconfig-paths/graph.golden.json +++ b/projectgraph/testdata/typescript/tsconfig-paths/graph.golden.json @@ -138,7 +138,7 @@ "src/main.ts": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 5, "edges": 4, diff --git a/projectgraph/testdata/typescript/tsconfig-paths/indexes.json b/projectgraph/testdata/typescript/tsconfig-paths/indexes.json index cad611f1..8b8e5a16 100644 --- a/projectgraph/testdata/typescript/tsconfig-paths/indexes.json +++ b/projectgraph/testdata/typescript/tsconfig-paths/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "src/main.ts", "language": "typescript", + "version": "14", "path": "src/main.ts", "language": "typescript", "imports": [ {"line": 1, "module": "@app/server", "item": "serve", "alias": "serve", "kind": "static", "namespace": "@app//server//serve"}, {"line": 2, "module": "src/config", "item": "cfg", "alias": "cfg", "kind": "static", "namespace": "src//config//cfg"} @@ -12,7 +12,7 @@ ] }, { - "version": "11", "path": "src/app/server.ts", "language": "typescript", + "version": "14", "path": "src/app/server.ts", "language": "typescript", "exports": [{"name": "serve", "target": "src/app/server.ts//serve", "line": 1}], "definitions": [ {"key": "src/app/server.ts", "kind": "module", "name": "server", "start_line": 1, "end_line": 1, "exported": true}, @@ -20,7 +20,7 @@ ] }, { - "version": "11", "path": "src/config.ts", "language": "typescript", + "version": "14", "path": "src/config.ts", "language": "typescript", "exports": [{"name": "cfg", "target": "src/config.ts//cfg", "line": 1}], "definitions": [ {"key": "src/config.ts", "kind": "module", "name": "config", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/testdata/typescript/type-only/graph.golden.json b/projectgraph/testdata/typescript/type-only/graph.golden.json index 0e27da62..327d10a3 100644 --- a/projectgraph/testdata/typescript/type-only/graph.golden.json +++ b/projectgraph/testdata/typescript/type-only/graph.golden.json @@ -97,7 +97,7 @@ "src/types.ts": "indexed" }, "stats": { - "version": "11", + "version": "16", "files": 3, "nodes": 5, "edges": 2, diff --git a/projectgraph/testdata/typescript/type-only/indexes.json b/projectgraph/testdata/typescript/type-only/indexes.json index 9c493a32..3dc37beb 100644 --- a/projectgraph/testdata/typescript/type-only/indexes.json +++ b/projectgraph/testdata/typescript/type-only/indexes.json @@ -1,6 +1,6 @@ [ { - "version": "11", "path": "src/main.ts", "language": "typescript", + "version": "14", "path": "src/main.ts", "language": "typescript", "imports": [ {"line": 1, "module": "./types", "item": "Opts", "alias": "Opts", "kind": "type-only", "namespace": ".//types//Opts"}, {"line": 2, "module": "./decl", "item": "decl", "alias": "decl", "kind": "static", "namespace": ".//decl//decl"} @@ -9,7 +9,7 @@ "calls": [{"caller": "src/main.ts", "line": 3, "column": 1, "callee": "decl", "targets": [".//decl//decl"], "kind": "call"}] }, { - "version": "11", "path": "src/types.ts", "language": "typescript", + "version": "14", "path": "src/types.ts", "language": "typescript", "exports": [{"name": "Opts", "target": "src/types.ts//Opts", "line": 1}], "definitions": [ {"key": "src/types.ts", "kind": "module", "name": "types", "start_line": 1, "end_line": 1, "exported": true}, @@ -17,7 +17,7 @@ ] }, { - "version": "11", "path": "src/decl.d.ts", "language": "typescript", + "version": "14", "path": "src/decl.d.ts", "language": "typescript", "exports": [{"name": "decl", "target": "src/decl.d.ts//decl", "line": 1}], "definitions": [ {"key": "src/decl.d.ts", "kind": "module", "name": "decl", "start_line": 1, "end_line": 1, "exported": true}, diff --git a/projectgraph/walk.go b/projectgraph/walk.go index 12191627..d0d32b73 100644 --- a/projectgraph/walk.go +++ b/projectgraph/walk.go @@ -65,11 +65,37 @@ func (b *builder) walkFrom(r resolution, names []string) resolution { case kind == NodeClass || kind == NodeInterface: consumed, done = b.walkClass(&r, name, i+1 == len(names), &instance) case kind == NodeExternal || kind == "" && b.g.IsExternal(r.key): + // A name before the last one under an external key is a call + // of a function or a method whose result has the next name, + // when a unit of dependency code gives the function. + if i+1 < len(names) { + // An unwrap that ends the names is the call of the method + // of the wrapper, so the walk takes it as a member. + if res, ok := b.externalResult(r.key, name); ok { + skip, typed := unwrap(res.wraps, names[i+1:]) + switch { + case !typed: + r.fail = &failure{reason: fileindex.UnresolvedUnknownReceiver, name: names[i+1]} + return r + case i+1+skip < len(names): + r.key, r.virtual, r.declared = res.class, true, true + i += 1 + skip + continue + } + } + // A type or a module of a unit gives the next name, as + // from_vec of smallvec::SmallVec::from_vec(v). + if defs, _ := b.unitDefinitions(derivedOrigins(b.origins[r.key], name)); len(defs) > 0 { + r.key = b.deriveKey(r.key, names[i:i+1]) + i++ + continue + } + } r.key = b.deriveExternal(r.key, names[i:]) r.rule = cmp.Or(r.rule, ruleExternal) return r case kind == NodeFunction || kind == NodeMethod || kind == NodeConstructor: - consumed, done = b.walkFunction(&r, name) + consumed, done = b.walkFunction(&r, names[i:]) default: r.fail = noDefinition(name) return r @@ -215,16 +241,24 @@ func (b *builder) walkClass(r *resolution, name string, last bool, instance *boo return false, true } -// walkFunction reads a name of a function: a Go call of the function gives -// a value of its first result, as New().Run(). Else the name is a local -// variable or a parameter of the function, and the index does not know its -// value. -func (b *builder) walkFunction(r *resolution, name string) (consumed, done bool) { - if class, ok := b.resultType(r.key); ok { - r.key, r.virtual, r.declared = class, true, true - return false, false +// walkFunction reads the names after a function: a call of the function +// gives a value of its first result, as New().Run() in Go. A Rust result +// in an Option or a Result takes unwrap or expect first. Else the name is a +// local variable or a parameter of the function, and the index does not +// know its value. +func (b *builder) walkFunction(r *resolution, names []string) (consumed, done bool) { + res, reason, ok := b.resultType(fileindex.PackageOf(r.key), r.key, 0) + skip, typed := unwrap(res.wraps, names) + switch { + case ok && typed && skip == 1 && len(names) == 1: + // An unwrap that ends the names runs the code of the function + // and the unwrap of std, so the walk ends at the function. + return true, false + case ok && typed: + r.key, r.virtual, r.declared = res.class, true, true + return skip == 1, false } - r.fail = &failure{reason: fileindex.UnresolvedUnknownReceiver, name: name} + r.fail = &failure{reason: cmp.Or(reason, fileindex.UnresolvedUnknownReceiver), name: names[0]} return false, true } diff --git a/scripts/fetch_method_corpus.sh b/scripts/fetch_method_corpus.sh new file mode 100755 index 00000000..7597a726 --- /dev/null +++ b/scripts/fetch_method_corpus.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Fetch the projects and the dependencies of the method measure into the +# directory of the first argument. Go modules come from the module proxy +# into /gomod, and crates from crates.io into /crates. The list +# is test/projectgraph/public/methods/corpus.txt. Point +# PROJECTGRAPH_METHOD_CORPUS at the directory to measure them. +set -euo pipefail + +dir="${1:?usage: fetch_method_corpus.sh }" +list="$(dirname "$0")/../test/projectgraph/public/methods/corpus.txt" +mkdir -p "$dir/gomod" "$dir/crates" +dir="$(cd "$dir" && pwd)" + +grep -v '^#' "$list" | while read -r language _ project deps; do + for item in $project $deps; do + case "$language" in + go) + GOFLAGS=-modcacherw GOMODCACHE="$dir/gomod" go mod download "$item" + ;; + rust) + name="${item%@*}" + version="${item#*@}" + if [ ! -d "$dir/crates/$name-$version" ]; then + curl -sSfL "https://static.crates.io/crates/$name/$name-$version.crate" | tar -xz -C "$dir/crates" + fi + ;; + esac + done +done +echo "$dir" diff --git a/test/projectgraph/README.md b/test/projectgraph/README.md index 62b56b4b..bc78a905 100644 --- a/test/projectgraph/README.md +++ b/test/projectgraph/README.md @@ -5,8 +5,8 @@ This directory tests the index step, the link step and the reach query of | Part | Files | Runs in | | --- | --- | --- | -| Hand-written cases | `cases///`, `cases_test.go`, `workers_test.go` (one output for any number of workers), `version_test.go` (the hash of the graphs and the summaries per version) | `go test ./...` | -| Public sets | `public/`, `public_test.go`, `govulncheck_test.go` | `go test -tags projectgraph` | +| Hand-written cases | `cases///`, `cases_test.go`, `receivers_test.go` (the forms of a method receiver), `workers_test.go` (one output for any number of workers), `version_test.go` (the hash of the graphs and the summaries per version) | `go test ./...` | +| Public sets | `public/`, `public_test.go`, `govulncheck_test.go`, `methods_test.go` | `go test -tags projectgraph` | | Benchmark | `bench_test.go`, `synth_test.go`, `synth_languages_test.go` (the C#, PHP, Ruby and Rust generators) | `go test -tags projectgraph -bench .` | `helpers_test.go` holds the index and link helpers that every part uses. @@ -116,6 +116,31 @@ go test -tags projectgraph -run TestGovulncheckTraces -count=1 -v ./test/project Set `PROJECTGRAPH_PUBLIC_DUMP` to a case name, as `classes/self_call`, or to `all`, to log the edges, the extra pairs and the missed pairs of the case. +## Run the method measure + +`methods_test.go` measures how often a target of a vulnerable function or +method of a dependency ends uncertain on real projects. +`public/methods/corpus.txt` lists the projects and the dependencies, and +`public/methods/advisories.json` holds their advisories from the Go vulndb +and from RustSec through OSV. The code is not in the repository, so fetch +it first: + +```bash +export PROJECTGRAPH_METHOD_CORPUS=$(bash scripts/fetch_method_corpus.sh "$(mktemp -d)") +go test -tags projectgraph -run TestMethodTargets -count=1 -v ./test/projectgraph/ +``` + +The test logs the count of each answer per language and per symbol kind, +and the same per project. Set `PROJECTGRAPH_METHOD_REPORT` to a file path +to write the block to that file. `report.md` says how the corpus was +chosen and what each cause means. + +`receivers_test.go` holds one row per form of the receiver of a method +call, for Go and for Rust, against the small dependency units under +`testdata/receivers`. It runs in `go test ./...`. A row that the link step +does not answer yet names the issue that fixes it and its answer before +that fix, so the test stays green until the fix changes the answer. + ## Run the benchmark ```bash diff --git a/test/projectgraph/cases/go/chained-external-call/expect.yaml b/test/projectgraph/cases/go/chained-external-call/expect.yaml new file mode 100644 index 00000000..dac25974 --- /dev/null +++ b/test/projectgraph/cases/go/chained-external-call/expect.yaml @@ -0,0 +1,69 @@ +source: safedep:hand +description: >- + run calls Next on the result of html.NewTokenizer, with no dependency code. The link step does not know the + type of that result, so the site keeps a record with the method Next. No unit gives the package, so the + reason is no-dependency-source. The + target Tokenizer.Next is then uncertain at the site, not no-such-target. The edge to the key + html//NewTokenizer//Next stays, because a Go call of a type name converts a value, and a method of the type + then runs. +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main + - key: tokens.go + kind: main + rule: go.main +exact_edges: true +edges: + - from: main.go//main + to: tokens.go//run + kind: direct + confidence: high + site: main.go:6 + - from: main.go//main + to: os//Stdin + kind: reference + confidence: low + site: main.go:6 + - from: tokens.go//run + to: golang.org//x//net//html//NewTokenizer + kind: direct + confidence: high + site: tokens.go:10 + - from: tokens.go//run + to: golang.org//x//net//html//NewTokenizer//Next + kind: direct + confidence: high + site: tokens.go:10 +unresolved: + - caller: tokens.go//run + site: tokens.go:10 + reason: no-dependency-source + method: Next +results: + - target: golang.org//x//net//html//Tokenizer//Next + status: uncertain + uncertainties: + - kind: unresolved-call + site: tokens.go:10 + - target: golang.org//x//net//html//NewTokenizer + status: found + path: + - key: main.go//main + - key: tokens.go//run + site: main.go:6 + kind: direct + confidence: high + - key: golang.org//x//net//html//NewTokenizer + site: tokens.go:10 + kind: direct + confidence: high + call_sites: + - caller: tokens.go//run + site: tokens.go:10 + reached: true + - target: golang.org//x//net//http2//Server//ServeConn + status: no-such-target diff --git a/test/projectgraph/cases/go/chained-external-call/project/go.mod b/test/projectgraph/cases/go/chained-external-call/project/go.mod new file mode 100644 index 00000000..af3f59c0 --- /dev/null +++ b/test/projectgraph/cases/go/chained-external-call/project/go.mod @@ -0,0 +1,5 @@ +module app + +go 1.22 + +require golang.org/x/net v0.61.0 diff --git a/test/projectgraph/cases/go/chained-external-call/project/main.go b/test/projectgraph/cases/go/chained-external-call/project/main.go new file mode 100644 index 00000000..eeb8f1fa --- /dev/null +++ b/test/projectgraph/cases/go/chained-external-call/project/main.go @@ -0,0 +1,7 @@ +package main + +import "os" + +func main() { + run(os.Stdin) +} diff --git a/test/projectgraph/cases/go/chained-external-call/project/tokens.go b/test/projectgraph/cases/go/chained-external-call/project/tokens.go new file mode 100644 index 00000000..fe62697c --- /dev/null +++ b/test/projectgraph/cases/go/chained-external-call/project/tokens.go @@ -0,0 +1,11 @@ +package main + +import ( + "io" + + "golang.org/x/net/html" +) + +func run(r io.Reader) { + html.NewTokenizer(r).Next() +} diff --git a/test/projectgraph/cases/go/dependency-build-files/expect.yaml b/test/projectgraph/cases/go/dependency-build-files/expect.yaml new file mode 100644 index 00000000..2f1d3c0f --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/expect.yaml @@ -0,0 +1,87 @@ +source: safedep:hand +description: >- + main calls Run of the unit github.com/acme/kit. No Go build reads two files of the unit, so the index step + leaves them out. gen.go has //go:build ignore and package main, and kit_test.go is a test file. So the link + step records no truncation for a second package clause, and their calls reach no graph. kit_windows.go has + //go:build windows and stays, because a reach answer must hold for every platform: its call of + syscall.CloseHandle is in the graph, and no root reaches it. tables.go is a generated file whose var holds a + string of 60 parts, past the depth limit of the index. Its index blanks the data value and keeps the body of + lookup, so the file is complete, gives no budget and keeps its calls. +units: + gomod/github.com/acme/kit@v1.0.0: go:github.com/acme/kit@v1.0.0 +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main +exact_edges: true +edges: + - from: main.go//main + to: github.com//acme//kit//Run + kind: direct + confidence: high + site: main.go:6 + - from: github.com//acme//kit//Run + to: go:github.com/acme/kit@v1.0.0//kit.go//Run + kind: package + confidence: high + site: go:github.com/acme/kit@v1.0.0//kit.go:5 + - from: go:github.com/acme/kit@v1.0.0//kit.go//Run + to: os//exec//Command + kind: direct + confidence: high + site: go:github.com/acme/kit@v1.0.0//kit.go:6 + - from: go:github.com/acme/kit@v1.0.0//kit.go//Run + to: go:github.com/acme/kit@v1.0.0//tables.go//lookup + kind: direct + confidence: high + site: go:github.com/acme/kit@v1.0.0//kit.go:7 + - from: go:github.com/acme/kit@v1.0.0//kit_windows.go//Close + to: syscall//CloseHandle + kind: direct + confidence: high + site: go:github.com/acme/kit@v1.0.0//kit_windows.go:8 + - from: go:github.com/acme/kit@v1.0.0//tables.go//lookup + to: len + kind: direct + confidence: high + site: go:github.com/acme/kit@v1.0.0//tables.go:6 + - from: go:github.com/acme/kit@v1.0.0//tables.go//lookup + to: len + kind: direct + confidence: high + site: go:github.com/acme/kit@v1.0.0//tables.go:6 +unresolved: [] +results: + - target: os//exec//Command + status: found + path: + - key: main.go//main + - key: github.com//acme//kit//Run + site: main.go:6 + kind: direct + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//Run + site: go:github.com/acme/kit@v1.0.0//kit.go:5 + kind: package + confidence: high + - key: os//exec//Command + site: go:github.com/acme/kit@v1.0.0//kit.go:6 + kind: direct + confidence: high + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//kit.go//Run + site: go:github.com/acme/kit@v1.0.0//kit.go:6 + reached: true + - target: os//RemoveAll + status: no-such-target + - target: os//WriteFile + status: no-such-target + - target: syscall//CloseHandle + status: not-found + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//kit_windows.go//Close + site: go:github.com/acme/kit@v1.0.0//kit_windows.go:8 + reached: false diff --git a/test/projectgraph/cases/go/dependency-build-files/project/go.mod b/test/projectgraph/cases/go/dependency-build-files/project/go.mod new file mode 100644 index 00000000..03907493 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/go.mod @@ -0,0 +1,5 @@ +module example.com/app + +go 1.22 + +require github.com/acme/kit v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/gen.go b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/gen.go new file mode 100644 index 00000000..c5f4217d --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/gen.go @@ -0,0 +1,9 @@ +//go:build ignore + +package main + +import "os" + +func main() { + os.WriteFile("tables.go", nil, 0o644) +} diff --git a/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/go.mod b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/go.mod new file mode 100644 index 00000000..529dd612 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/go.mod @@ -0,0 +1,3 @@ +module github.com/acme/kit + +go 1.22 diff --git a/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit.go b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit.go new file mode 100644 index 00000000..d1844170 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit.go @@ -0,0 +1,8 @@ +package kit + +import "os/exec" + +func Run(name string) { + _ = exec.Command(name) + _ = lookup(name) +} diff --git a/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit_test.go b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit_test.go new file mode 100644 index 00000000..e126828b --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit_test.go @@ -0,0 +1,11 @@ +package kit + +import ( + "os" + "testing" +) + +func TestRun(t *testing.T) { + os.RemoveAll(t.TempDir()) + Run("a") +} diff --git a/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit_windows.go b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit_windows.go new file mode 100644 index 00000000..51d664ed --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/kit_windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package kit + +import "syscall" + +func Close() { + syscall.CloseHandle(0) +} diff --git a/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/tables.go b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/tables.go new file mode 100644 index 00000000..54d6e94c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/gomod/github.com/acme/kit@v1.0.0/tables.go @@ -0,0 +1,9 @@ +// Code generated by gen.go. DO NOT EDIT. + +package kit + +func lookup(name string) int { + return len(names) + len(name) +} + +var names string = "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" diff --git a/test/projectgraph/cases/go/dependency-build-files/project/main.go b/test/projectgraph/cases/go/dependency-build-files/project/main.go new file mode 100644 index 00000000..78434045 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/project/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/acme/kit" + +func main() { + kit.Run("a") +} diff --git a/test/projectgraph/cases/go/dependency-build-files/summaries/go_github.com_acme_kit_v1.0.0.json b/test/projectgraph/cases/go/dependency-build-files/summaries/go_github.com_acme_kit_v1.0.0.json new file mode 100644 index 00000000..d75c8c99 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-build-files/summaries/go_github.com_acme_kit_v1.0.0.json @@ -0,0 +1,30 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:github.com/acme/kit@v1.0.0", + "symbols": [ + { + "key": "github.com//acme//kit//Close", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//kit_windows.go//Close" + ], + "reaches": [ + "syscall//CloseHandle" + ] + }, + { + "key": "github.com//acme//kit//Run", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//kit.go//Run" + ], + "reaches": [ + "os//exec//Command" + ] + } + ], + "targets": [ + "os//exec//Command", + "syscall//CloseHandle" + ] +} diff --git a/test/projectgraph/cases/go/dependency-generated-code/expect.yaml b/test/projectgraph/cases/go/dependency-generated-code/expect.yaml new file mode 100644 index 00000000..f2a52f83 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/expect.yaml @@ -0,0 +1,41 @@ +source: safedep:hand +description: >- + main calls Run of the unit, and Run calls gen in gen.go, a generated file. Its var holds a string of 60 + parts, past the depth limit of the index. The index blanks the data value and keeps the body of gen, so the + call of exec.Command stays in the graph and reach finds it. A skeleton that dropped the bodies gave + no-such-target. +units: + gomod/github.com/acme/kit@v1.0.0: go:github.com/acme/kit@v1.0.0 +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main +unresolved: [] +results: + - target: os//exec//Command + status: found + path: + - key: main.go//main + - key: github.com//acme//kit//Run + site: main.go:6 + kind: direct + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//Run + site: go:github.com/acme/kit@v1.0.0//kit.go:3 + kind: package + confidence: high + - key: go:github.com/acme/kit@v1.0.0//gen.go//gen + site: go:github.com/acme/kit@v1.0.0//kit.go:3 + kind: direct + confidence: high + - key: os//exec//Command + site: go:github.com/acme/kit@v1.0.0//gen.go:7 + kind: direct + confidence: high + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//gen.go//gen + site: go:github.com/acme/kit@v1.0.0//gen.go:7 + reached: true diff --git a/test/projectgraph/cases/go/dependency-generated-code/project/go.mod b/test/projectgraph/cases/go/dependency-generated-code/project/go.mod new file mode 100644 index 00000000..c7090c9c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/project/go.mod @@ -0,0 +1,5 @@ +module app + +go 1.22 + +require github.com/acme/kit v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/gen.go b/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/gen.go new file mode 100644 index 00000000..6f91564d --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/gen.go @@ -0,0 +1,9 @@ +// Code generated by synth. DO NOT EDIT. + +package kit + +import "os/exec" + +func gen() { _ = exec.Command("x") } + +var data string = "" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" + "x" diff --git a/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/go.mod b/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/go.mod new file mode 100644 index 00000000..529dd612 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/go.mod @@ -0,0 +1,3 @@ +module github.com/acme/kit + +go 1.22 diff --git a/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/kit.go b/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/kit.go new file mode 100644 index 00000000..8e7a76b3 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/project/gomod/github.com/acme/kit@v1.0.0/kit.go @@ -0,0 +1,3 @@ +package kit + +func Run() { gen() } diff --git a/test/projectgraph/cases/go/dependency-generated-code/project/main.go b/test/projectgraph/cases/go/dependency-generated-code/project/main.go new file mode 100644 index 00000000..fc95b081 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/project/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/acme/kit" + +func main() { + kit.Run() +} diff --git a/test/projectgraph/cases/go/dependency-generated-code/summaries/go_github.com_acme_kit_v1.0.0.json b/test/projectgraph/cases/go/dependency-generated-code/summaries/go_github.com_acme_kit_v1.0.0.json new file mode 100644 index 00000000..bd84fe61 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-generated-code/summaries/go_github.com_acme_kit_v1.0.0.json @@ -0,0 +1,20 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:github.com/acme/kit@v1.0.0", + "symbols": [ + { + "key": "github.com//acme//kit//Run", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//kit.go//Run" + ], + "reaches": [ + "os//exec//Command" + ] + } + ], + "targets": [ + "os//exec//Command" + ] +} diff --git a/test/projectgraph/cases/go/dependency-implements-interface/expect.yaml b/test/projectgraph/cases/go/dependency-implements-interface/expect.yaml index 78d245e8..253cdef4 100644 --- a/test/projectgraph/cases/go/dependency-implements-interface/expect.yaml +++ b/test/projectgraph/cases/go/dependency-implements-interface/expect.yaml @@ -2,7 +2,9 @@ source: safedep:hand description: >- main holds a value of the project interface Runner and calls Run on it. The value comes from New of the dependency dep, whose type Impl implements Runner and calls exec.Command in Run. The interface is - first-party, so Impl.Run of the unit is a cha candidate, and the site keeps its record. The record has + first-party, so Impl.Run of the unit is a cha candidate, and the site keeps its record. The cha edge goes to + the external key of Impl.Run, so a target of that key finds the path, and a package edge joins the key to + the definition. The record has the method Run, and Impl.Run reaches exec.Command, so the target is never not found. units: gomod/github.com/acme/dep@v1.0.0: go:github.com/acme/dep@v1.0.0 @@ -26,10 +28,15 @@ edges: confidence: high site: go:github.com/acme/dep@v1.0.0//dep.go:7 - from: main.go//main - to: go:github.com/acme/dep@v1.0.0//dep.go//Impl//Run + to: github.com//acme//dep//Impl//Run kind: cha confidence: medium site: main.go:10 + - from: github.com//acme//dep//Impl//Run + to: go:github.com/acme/dep@v1.0.0//dep.go//Impl//Run + kind: package + confidence: high + site: go:github.com/acme/dep@v1.0.0//dep.go:11 - from: go:github.com/acme/dep@v1.0.0//dep.go//Impl//Run to: os//exec//Command kind: direct @@ -45,10 +52,14 @@ results: status: found path: - key: main.go//main - - key: go:github.com/acme/dep@v1.0.0//dep.go//Impl//Run + - key: github.com//acme//dep//Impl//Run site: main.go:10 kind: cha confidence: medium + - key: go:github.com/acme/dep@v1.0.0//dep.go//Impl//Run + site: go:github.com/acme/dep@v1.0.0//dep.go:11 + kind: package + confidence: high - key: os//exec//Command site: go:github.com/acme/dep@v1.0.0//dep.go:12 kind: direct @@ -57,3 +68,15 @@ results: - caller: go:github.com/acme/dep@v1.0.0//dep.go//Impl//Run site: go:github.com/acme/dep@v1.0.0//dep.go:12 reached: true + - target: github.com//acme//dep//Impl//Run + status: found + path: + - key: main.go//main + - key: github.com//acme//dep//Impl//Run + site: main.go:10 + kind: cha + confidence: medium + call_sites: + - caller: main.go//main + site: main.go:10 + reached: true diff --git a/test/projectgraph/cases/go/dependency-implements-interface/summaries/go_github.com_acme_dep_v1.0.0.json b/test/projectgraph/cases/go/dependency-implements-interface/summaries/go_github.com_acme_dep_v1.0.0.json index bdce1cd7..69acb2cf 100644 --- a/test/projectgraph/cases/go/dependency-implements-interface/summaries/go_github.com_acme_dep_v1.0.0.json +++ b/test/projectgraph/cases/go/dependency-implements-interface/summaries/go_github.com_acme_dep_v1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "go:github.com/acme/dep@v1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/go/dependency-internal-call/expect.yaml b/test/projectgraph/cases/go/dependency-internal-call/expect.yaml new file mode 100644 index 00000000..5aff8146 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-internal-call/expect.yaml @@ -0,0 +1,69 @@ +source: safedep:hand +description: >- + main calls Serve of the unit github.com/acme/kit. Serve calls the method readFrame of an unexported type, and + readFrame calls the unexported parseHeader. No file of the project names parseHeader, so the graph has no + external node for it. The target github.com//acme//kit//parseHeader matches the definition of parseHeader in the + unit by its external key, so reach finds the path through Serve and readFrame. Without the match the answer was + no-such-target, and with a reference from unreached project code it was not-found. Dead is called only from the + unexported dead, which nothing calls, so it is not-found. +units: + gomod/github.com/acme/kit@v1.0.0: go:github.com/acme/kit@v1.0.0 +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main +unresolved: [] +results: + - target: github.com//acme//kit//parseHeader + status: found + path: + - key: main.go//main + - key: github.com//acme//kit//Serve + site: main.go:6 + kind: direct + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//Serve + site: go:github.com/acme/kit@v1.0.0//kit.go:3 + kind: package + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//conn//readFrame + site: go:github.com/acme/kit@v1.0.0//kit.go:5 + kind: member + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//parseHeader + site: go:github.com/acme/kit@v1.0.0//kit.go:10 + kind: direct + confidence: high + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//kit.go//conn//readFrame + site: go:github.com/acme/kit@v1.0.0//kit.go:10 + reached: true + - target: github.com//acme//kit//conn//readFrame + status: found + path: + - key: main.go//main + - key: github.com//acme//kit//Serve + site: main.go:6 + kind: direct + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//Serve + site: go:github.com/acme/kit@v1.0.0//kit.go:3 + kind: package + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//conn//readFrame + site: go:github.com/acme/kit@v1.0.0//kit.go:5 + kind: member + confidence: high + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//kit.go//Serve + site: go:github.com/acme/kit@v1.0.0//kit.go:5 + reached: true + - target: github.com//acme//kit//Dead + status: not-found + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//kit.go//dead + site: go:github.com/acme/kit@v1.0.0//kit.go:16 + reached: false \ No newline at end of file diff --git a/test/projectgraph/cases/go/dependency-internal-call/project/go.mod b/test/projectgraph/cases/go/dependency-internal-call/project/go.mod new file mode 100644 index 00000000..c7090c9c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-internal-call/project/go.mod @@ -0,0 +1,5 @@ +module app + +go 1.22 + +require github.com/acme/kit v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-internal-call/project/gomod/github.com/acme/kit@v1.0.0/go.mod b/test/projectgraph/cases/go/dependency-internal-call/project/gomod/github.com/acme/kit@v1.0.0/go.mod new file mode 100644 index 00000000..529dd612 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-internal-call/project/gomod/github.com/acme/kit@v1.0.0/go.mod @@ -0,0 +1,3 @@ +module github.com/acme/kit + +go 1.22 diff --git a/test/projectgraph/cases/go/dependency-internal-call/project/gomod/github.com/acme/kit@v1.0.0/kit.go b/test/projectgraph/cases/go/dependency-internal-call/project/gomod/github.com/acme/kit@v1.0.0/kit.go new file mode 100644 index 00000000..b825a79c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-internal-call/project/gomod/github.com/acme/kit@v1.0.0/kit.go @@ -0,0 +1,16 @@ +package kit + +func Serve() { + var c conn + c.readFrame() +} + +type conn struct{} + +func (c *conn) readFrame() { parseHeader() } + +func parseHeader() {} + +func Dead() {} + +func dead() { Dead() } diff --git a/test/projectgraph/cases/go/dependency-internal-call/project/main.go b/test/projectgraph/cases/go/dependency-internal-call/project/main.go new file mode 100644 index 00000000..5210efdf --- /dev/null +++ b/test/projectgraph/cases/go/dependency-internal-call/project/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/acme/kit" + +func main() { + kit.Serve() +} diff --git a/test/projectgraph/cases/go/dependency-internal-call/summaries/go_github.com_acme_kit_v1.0.0.json b/test/projectgraph/cases/go/dependency-internal-call/summaries/go_github.com_acme_kit_v1.0.0.json new file mode 100644 index 00000000..e8235fda --- /dev/null +++ b/test/projectgraph/cases/go/dependency-internal-call/summaries/go_github.com_acme_kit_v1.0.0.json @@ -0,0 +1,23 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:github.com/acme/kit@v1.0.0", + "symbols": [ + { + "key": "github.com//acme//kit//Dead", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//kit.go//Dead" + ], + "reaches": [] + }, + { + "key": "github.com//acme//kit//Serve", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//kit.go//Serve" + ], + "reaches": [] + } + ], + "targets": [] +} diff --git a/test/projectgraph/cases/go/dependency-module/summaries/go_github.com_acme_yamlish_v1.2.0.json b/test/projectgraph/cases/go/dependency-module/summaries/go_github.com_acme_yamlish_v1.2.0.json index f1621e24..c27b2ee8 100644 --- a/test/projectgraph/cases/go/dependency-module/summaries/go_github.com_acme_yamlish_v1.2.0.json +++ b/test/projectgraph/cases/go/dependency-module/summaries/go_github.com_acme_yamlish_v1.2.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "go:github.com/acme/yamlish@v1.2.0", "symbols": [ { diff --git a/test/projectgraph/cases/go/dependency-platform-files/expect.yaml b/test/projectgraph/cases/go/dependency-platform-files/expect.yaml new file mode 100644 index 00000000..7b80a2c5 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/expect.yaml @@ -0,0 +1,40 @@ +source: safedep:hand +description: >- + main calls Run of the unit, and Run calls platform. kit_linux.go and kit_darwin.go each define platform, + and only the darwin twin calls exec.Command. The index keeps both twins, because a reach answer must hold + for every platform, so reach finds the call. An index of linux/amd64 alone gave no-such-target. +units: + gomod/github.com/acme/kit@v1.0.0: go:github.com/acme/kit@v1.0.0 +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main +unresolved: [] +results: + - target: os//exec//Command + status: found + path: + - key: main.go//main + - key: github.com//acme//kit//Run + site: main.go:6 + kind: direct + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit.go//Run + site: go:github.com/acme/kit@v1.0.0//kit.go:3 + kind: package + confidence: high + - key: go:github.com/acme/kit@v1.0.0//kit_darwin.go//platform + site: go:github.com/acme/kit@v1.0.0//kit.go:3 + kind: direct + confidence: high + - key: os//exec//Command + site: go:github.com/acme/kit@v1.0.0//kit_darwin.go:5 + kind: direct + confidence: high + call_sites: + - caller: go:github.com/acme/kit@v1.0.0//kit_darwin.go//platform + site: go:github.com/acme/kit@v1.0.0//kit_darwin.go:5 + reached: true diff --git a/test/projectgraph/cases/go/dependency-platform-files/project/go.mod b/test/projectgraph/cases/go/dependency-platform-files/project/go.mod new file mode 100644 index 00000000..c7090c9c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/project/go.mod @@ -0,0 +1,5 @@ +module app + +go 1.22 + +require github.com/acme/kit v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/go.mod b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/go.mod new file mode 100644 index 00000000..529dd612 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/go.mod @@ -0,0 +1,3 @@ +module github.com/acme/kit + +go 1.22 diff --git a/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit.go b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit.go new file mode 100644 index 00000000..de7427c6 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit.go @@ -0,0 +1,3 @@ +package kit + +func Run() { platform() } diff --git a/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit_darwin.go b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit_darwin.go new file mode 100644 index 00000000..cd40708f --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit_darwin.go @@ -0,0 +1,5 @@ +package kit + +import "os/exec" + +func platform() { _ = exec.Command("x") } diff --git a/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit_linux.go b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit_linux.go new file mode 100644 index 00000000..22509de1 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/project/gomod/github.com/acme/kit@v1.0.0/kit_linux.go @@ -0,0 +1,3 @@ +package kit + +func platform() {} diff --git a/test/projectgraph/cases/go/dependency-platform-files/project/main.go b/test/projectgraph/cases/go/dependency-platform-files/project/main.go new file mode 100644 index 00000000..fc95b081 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/project/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/acme/kit" + +func main() { + kit.Run() +} diff --git a/test/projectgraph/cases/go/dependency-platform-files/summaries/go_github.com_acme_kit_v1.0.0.json b/test/projectgraph/cases/go/dependency-platform-files/summaries/go_github.com_acme_kit_v1.0.0.json new file mode 100644 index 00000000..bd84fe61 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-files/summaries/go_github.com_acme_kit_v1.0.0.json @@ -0,0 +1,20 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:github.com/acme/kit@v1.0.0", + "symbols": [ + { + "key": "github.com//acme//kit//Run", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//kit.go//Run" + ], + "reaches": [ + "os//exec//Command" + ] + } + ], + "targets": [ + "os//exec//Command" + ] +} diff --git a/test/projectgraph/cases/go/dependency-platform-results/expect.yaml b/test/projectgraph/cases/go/dependency-platform-results/expect.yaml new file mode 100644 index 00000000..7efe2e58 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/expect.yaml @@ -0,0 +1,37 @@ +source: safedep:hand +description: >- + new_linux.go and new_windows.go each define New, and they return the types Linux and Windows. Both types have + the method Common. The walk follows one result type, so the call New().Common() is a record with the reason + unknown-receiver. Before, the walk took the type of the first file, and Windows//Common was not-found. +units: + gomod/github.com/acme/kit@v1.0.0: go:github.com/acme/kit@v1.0.0 +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main +unresolved: + - caller: github.com//acme//kit//New//Common + site: go:github.com/acme/kit@v1.0.0 + reason: unknown-receiver + - caller: main.go//main + site: main.go:6 + reason: unknown-receiver + method: Common +results: + - target: github.com//acme//kit//Windows//Common + status: uncertain + uncertainties: + - kind: unresolved-call + site: main.go:6 + - kind: unresolved-call + site: go:github.com/acme/kit@v1.0.0 + - target: github.com//acme//kit//Linux//Common + status: uncertain + uncertainties: + - kind: unresolved-call + site: main.go:6 + - kind: unresolved-call + site: go:github.com/acme/kit@v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-platform-results/project/go.mod b/test/projectgraph/cases/go/dependency-platform-results/project/go.mod new file mode 100644 index 00000000..c7090c9c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/project/go.mod @@ -0,0 +1,5 @@ +module app + +go 1.22 + +require github.com/acme/kit v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/go.mod b/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/go.mod new file mode 100644 index 00000000..529dd612 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/go.mod @@ -0,0 +1,3 @@ +module github.com/acme/kit + +go 1.22 diff --git a/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/new_linux.go b/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/new_linux.go new file mode 100644 index 00000000..27f8ead1 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/new_linux.go @@ -0,0 +1,7 @@ +package kit + +type Linux struct{} + +func (*Linux) Common() {} + +func New() *Linux { return &Linux{} } diff --git a/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/new_windows.go b/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/new_windows.go new file mode 100644 index 00000000..7268f125 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/project/gomod/github.com/acme/kit@v1.0.0/new_windows.go @@ -0,0 +1,9 @@ +package kit + +type Windows struct{} + +func (*Windows) Common() {} + +func (*Windows) WindowsOnly() {} + +func New() *Windows { return &Windows{} } diff --git a/test/projectgraph/cases/go/dependency-platform-results/project/main.go b/test/projectgraph/cases/go/dependency-platform-results/project/main.go new file mode 100644 index 00000000..5d9844d5 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/project/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/acme/kit" + +func main() { + kit.New().Common() +} diff --git a/test/projectgraph/cases/go/dependency-platform-results/summaries/go_github.com_acme_kit_v1.0.0.json b/test/projectgraph/cases/go/dependency-platform-results/summaries/go_github.com_acme_kit_v1.0.0.json new file mode 100644 index 00000000..bc6a5746 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-platform-results/summaries/go_github.com_acme_kit_v1.0.0.json @@ -0,0 +1,51 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:github.com/acme/kit@v1.0.0", + "symbols": [ + { + "key": "github.com//acme//kit//Linux", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//new_linux.go//Linux" + ], + "reaches": [] + }, + { + "key": "github.com//acme//kit//Linux//Common", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//new_linux.go//Linux//Common" + ], + "reaches": [] + }, + { + "key": "github.com//acme//kit//New", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//new_linux.go//New" + ], + "reaches": [] + }, + { + "key": "github.com//acme//kit//Windows", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//new_windows.go//Windows" + ], + "reaches": [] + }, + { + "key": "github.com//acme//kit//Windows//Common", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//new_windows.go//Windows//Common" + ], + "reaches": [] + }, + { + "key": "github.com//acme//kit//Windows//WindowsOnly", + "definitions": [ + "go:github.com/acme/kit@v1.0.0//new_windows.go//Windows//WindowsOnly" + ], + "reaches": [] + } + ], + "targets": [] +} diff --git a/test/projectgraph/cases/go/dependency-result-types/expect.yaml b/test/projectgraph/cases/go/dependency-result-types/expect.yaml new file mode 100644 index 00000000..f81aaaeb --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/expect.yaml @@ -0,0 +1,218 @@ +source: safedep:hand +description: >- + tokens holds z := markup.NewTokenizer(nil) and calls z.Next, and calls Close on the result of Raw on the result + of NewTokenizer. tree calls Render on the first result of Parse, and send calls Write on the first result of + http.NewRequest. The unit of markup gives the result type of each function and method, and the unit of the + standard library, as GOROOT/src gives it, gives the type of NewRequest. So each call gets the external key of + the method of the type, as example.com//markup//Tokenizer//Next, and a package edge joins the key to the + definition of the unit. +units: + gomod/example.com/markup@v1.0.0: go:example.com/markup@v1.0.0 + goroot/src: go:std@go1.25.1 +roots: + - key: main.go + kind: main + rule: go.main + - key: main.go//main + kind: main + rule: go.main +exact_edges: true +edges: + - from: example.com//markup//NewTokenizer + to: go:example.com/markup@v1.0.0//token.go//NewTokenizer + kind: package + confidence: high + site: go:example.com/markup@v1.0.0//token.go:14 + - from: example.com//markup//Node//Render + to: go:example.com/markup@v1.0.0//node.go//Node//Render + kind: package + confidence: high + site: go:example.com/markup@v1.0.0//node.go:14 + - from: example.com//markup//Parse + to: go:example.com/markup@v1.0.0//node.go//Parse + kind: package + confidence: high + site: go:example.com/markup@v1.0.0//node.go:10 + - from: example.com//markup//Text//Close + to: go:example.com/markup@v1.0.0//token.go//Text//Close + kind: package + confidence: high + site: go:example.com/markup@v1.0.0//token.go:27 + - from: example.com//markup//Tokenizer//Next + to: go:example.com/markup@v1.0.0//token.go//Tokenizer//Next + kind: package + confidence: high + site: go:example.com/markup@v1.0.0//token.go:18 + - from: example.com//markup//Tokenizer//Raw + to: go:example.com/markup@v1.0.0//token.go//Tokenizer//Raw + kind: package + confidence: high + site: go:example.com/markup@v1.0.0//token.go:23 + - from: go:example.com/markup@v1.0.0//node.go//Node//Render + to: os//Remove + kind: direct + confidence: high + site: go:example.com/markup@v1.0.0//node.go:15 + - from: go:example.com/markup@v1.0.0//token.go//Text//Close + to: os//exec//Command + kind: direct + confidence: high + site: go:example.com/markup@v1.0.0//token.go:28 + - from: go:example.com/markup@v1.0.0//token.go//Tokenizer//Next + to: os//exec//Command + kind: direct + confidence: high + site: go:example.com/markup@v1.0.0//token.go:19 + - from: go:std@go1.25.1//net/http/request.go//Request//Write + to: go:std@go1.25.1//os/file.go//Getenv + kind: direct + confidence: high + site: go:std@go1.25.1//net/http/request.go:15 + - from: main.go//main + to: main.go//tokens + kind: direct + confidence: high + site: main.go:10 + - from: main.go//main + to: main.go//tree + kind: direct + confidence: high + site: main.go:11 + - from: main.go//main + to: main.go//send + kind: direct + confidence: high + site: main.go:12 + - from: main.go//send + to: net//http//NewRequest + kind: direct + confidence: high + site: main.go:30 + - from: main.go//send + to: net//http//Request//Write + kind: direct + confidence: high + site: main.go:31 + - from: main.go//tokens + to: example.com//markup//NewTokenizer + kind: direct + confidence: high + site: main.go:16 + - from: main.go//tokens + to: example.com//markup//Tokenizer//Next + kind: direct + confidence: high + site: main.go:17 + - from: main.go//tokens + to: example.com//markup//NewTokenizer + kind: direct + confidence: high + site: main.go:18 + - from: main.go//tokens + to: example.com//markup//Text//Close + kind: direct + confidence: high + site: main.go:18 + - from: main.go//tokens + to: example.com//markup//Tokenizer//Raw + kind: direct + confidence: high + site: main.go:18 + - from: main.go//tree + to: example.com//markup//Parse + kind: direct + confidence: high + site: main.go:22 + - from: main.go//tree + to: example.com//markup//Node//Render + kind: direct + confidence: high + site: main.go:26 + - from: net//http//NewRequest + to: go:std@go1.25.1//net/http/request.go//NewRequest + kind: package + confidence: high + site: go:std@go1.25.1//net/http/request.go:10 + - from: net//http//Request//Write + to: go:std@go1.25.1//net/http/request.go//Request//Write + kind: package + confidence: high + site: go:std@go1.25.1//net/http/request.go:14 + - from: os//Remove + to: go:std@go1.25.1//os/file.go//Remove + kind: package + confidence: high + site: go:std@go1.25.1//os/file.go:7 + - from: os//exec//Command + to: go:std@go1.25.1//os/exec/exec.go//Command + kind: package + confidence: high + site: go:std@go1.25.1//os/exec/exec.go:5 +unresolved: [] +results: + - target: example.com//markup//Tokenizer//Next + status: found + path: + - key: main.go//main + - key: main.go//tokens + site: main.go:10 + kind: direct + confidence: high + - key: example.com//markup//Tokenizer//Next + site: main.go:17 + kind: direct + confidence: high + call_sites: + - caller: main.go//tokens + site: main.go:17 + reached: true + - target: example.com//markup//Text//Close + status: found + path: + - key: main.go//main + - key: main.go//tokens + site: main.go:10 + kind: direct + confidence: high + - key: example.com//markup//Text//Close + site: main.go:18 + kind: direct + confidence: high + call_sites: + - caller: main.go//tokens + site: main.go:18 + reached: true + - target: example.com//markup//Node//Render + status: found + path: + - key: main.go//main + - key: main.go//tree + site: main.go:11 + kind: direct + confidence: high + - key: example.com//markup//Node//Render + site: main.go:26 + kind: direct + confidence: high + call_sites: + - caller: main.go//tree + site: main.go:26 + reached: true + - target: net//http//Request//Write + status: found + path: + - key: main.go//main + - key: main.go//send + site: main.go:12 + kind: direct + confidence: high + - key: net//http//Request//Write + site: main.go:31 + kind: direct + confidence: high + call_sites: + - caller: main.go//send + site: main.go:31 + reached: true + - target: example.com//markup//NewTokenizer//Next + status: no-such-target diff --git a/test/projectgraph/cases/go/dependency-result-types/project/go.mod b/test/projectgraph/cases/go/dependency-result-types/project/go.mod new file mode 100644 index 00000000..b4b143f8 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/go.mod @@ -0,0 +1,5 @@ +module example.com/app + +go 1.22 + +require example.com/markup v1.0.0 diff --git a/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/go.mod b/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/go.mod new file mode 100644 index 00000000..2faaaa0c --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/go.mod @@ -0,0 +1,3 @@ +module example.com/markup + +go 1.22 diff --git a/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/node.go b/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/node.go new file mode 100644 index 00000000..2ec57a04 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/node.go @@ -0,0 +1,16 @@ +package markup + +import ( + "io" + "os" +) + +type Node struct{} + +func Parse(r io.Reader) (*Node, error) { + return &Node{}, nil +} + +func (n *Node) Render(w io.Writer) { + os.Remove("render") +} diff --git a/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/token.go b/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/token.go new file mode 100644 index 00000000..6433c2c2 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/gomod/example.com/markup@v1.0.0/token.go @@ -0,0 +1,29 @@ +package markup + +import ( + "io" + "os/exec" +) + +type Tokenizer struct { + r io.Reader +} + +type Text struct{} + +func NewTokenizer(r io.Reader) *Tokenizer { + return &Tokenizer{r: r} +} + +func (z *Tokenizer) Next() int { + exec.Command("next") + return 0 +} + +func (z *Tokenizer) Raw() *Text { + return &Text{} +} + +func (t *Text) Close() { + exec.Command("close") +} diff --git a/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/go.mod b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/go.mod new file mode 100644 index 00000000..1ded0318 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/go.mod @@ -0,0 +1,3 @@ +module std + +go 1.25 diff --git a/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/io/io.go b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/io/io.go new file mode 100644 index 00000000..bb54e08e --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/io/io.go @@ -0,0 +1,9 @@ +package io + +type Reader interface { + Read(p []byte) (int, error) +} + +type Writer interface { + Write(p []byte) (int, error) +} diff --git a/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/net/http/request.go b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/net/http/request.go new file mode 100644 index 00000000..8e9af287 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/net/http/request.go @@ -0,0 +1,17 @@ +package http + +import ( + "io" + "os" +) + +type Request struct{} + +func NewRequest(method, url string, body io.Reader) (*Request, error) { + return &Request{}, nil +} + +func (r *Request) Write(w io.Writer) error { + os.Getenv("write") + return nil +} diff --git a/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/os/exec/exec.go b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/os/exec/exec.go new file mode 100644 index 00000000..2e151876 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/os/exec/exec.go @@ -0,0 +1,7 @@ +package exec + +type Cmd struct{} + +func Command(name string, arg ...string) *Cmd { + return &Cmd{} +} diff --git a/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/os/file.go b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/os/file.go new file mode 100644 index 00000000..070605f5 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/goroot/src/os/file.go @@ -0,0 +1,9 @@ +package os + +func Getenv(key string) string { + return "" +} + +func Remove(name string) error { + return nil +} diff --git a/test/projectgraph/cases/go/dependency-result-types/project/main.go b/test/projectgraph/cases/go/dependency-result-types/project/main.go new file mode 100644 index 00000000..bb6aa6fe --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/project/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "net/http" + + "example.com/markup" +) + +func main() { + tokens() + tree() + send() +} + +func tokens() { + z := markup.NewTokenizer(nil) + z.Next() + markup.NewTokenizer(nil).Raw().Close() +} + +func tree() { + n, err := markup.Parse(nil) + if err != nil { + return + } + n.Render(nil) +} + +func send() { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Write(nil) +} diff --git a/test/projectgraph/cases/go/dependency-result-types/summaries/go_example.com_markup_v1.0.0.json b/test/projectgraph/cases/go/dependency-result-types/summaries/go_example.com_markup_v1.0.0.json new file mode 100644 index 00000000..3ecc19d6 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/summaries/go_example.com_markup_v1.0.0.json @@ -0,0 +1,81 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:example.com/markup@v1.0.0", + "symbols": [ + { + "key": "example.com//markup//NewTokenizer", + "definitions": [ + "go:example.com/markup@v1.0.0//token.go//NewTokenizer" + ], + "reaches": [] + }, + { + "key": "example.com//markup//Node", + "definitions": [ + "go:example.com/markup@v1.0.0//node.go//Node" + ], + "reaches": [] + }, + { + "key": "example.com//markup//Node//Render", + "definitions": [ + "go:example.com/markup@v1.0.0//node.go//Node//Render" + ], + "reaches": [ + "os//Remove" + ] + }, + { + "key": "example.com//markup//Parse", + "definitions": [ + "go:example.com/markup@v1.0.0//node.go//Parse" + ], + "reaches": [] + }, + { + "key": "example.com//markup//Text", + "definitions": [ + "go:example.com/markup@v1.0.0//token.go//Text" + ], + "reaches": [] + }, + { + "key": "example.com//markup//Text//Close", + "definitions": [ + "go:example.com/markup@v1.0.0//token.go//Text//Close" + ], + "reaches": [ + "os//exec//Command" + ] + }, + { + "key": "example.com//markup//Tokenizer", + "definitions": [ + "go:example.com/markup@v1.0.0//token.go//Tokenizer" + ], + "reaches": [] + }, + { + "key": "example.com//markup//Tokenizer//Next", + "definitions": [ + "go:example.com/markup@v1.0.0//token.go//Tokenizer//Next" + ], + "reaches": [ + "os//exec//Command" + ] + }, + { + "key": "example.com//markup//Tokenizer//Raw", + "definitions": [ + "go:example.com/markup@v1.0.0//token.go//Tokenizer//Raw" + ], + "reaches": [] + } + ], + "targets": [ + "os//Remove", + "os//exec//Command" + ] +} diff --git a/test/projectgraph/cases/go/dependency-result-types/summaries/go_std_go1.25.1.json b/test/projectgraph/cases/go/dependency-result-types/summaries/go_std_go1.25.1.json new file mode 100644 index 00000000..a138a779 --- /dev/null +++ b/test/projectgraph/cases/go/dependency-result-types/summaries/go_std_go1.25.1.json @@ -0,0 +1,72 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "go:std@go1.25.1", + "symbols": [ + { + "key": "io//Reader", + "definitions": [ + "go:std@go1.25.1//io/io.go//Reader" + ], + "reaches": [] + }, + { + "key": "io//Writer", + "definitions": [ + "go:std@go1.25.1//io/io.go//Writer" + ], + "reaches": [] + }, + { + "key": "net//http//NewRequest", + "definitions": [ + "go:std@go1.25.1//net/http/request.go//NewRequest" + ], + "reaches": [] + }, + { + "key": "net//http//Request", + "definitions": [ + "go:std@go1.25.1//net/http/request.go//Request" + ], + "reaches": [] + }, + { + "key": "net//http//Request//Write", + "definitions": [ + "go:std@go1.25.1//net/http/request.go//Request//Write" + ], + "reaches": [] + }, + { + "key": "os//Getenv", + "definitions": [ + "go:std@go1.25.1//os/file.go//Getenv" + ], + "reaches": [] + }, + { + "key": "os//Remove", + "definitions": [ + "go:std@go1.25.1//os/file.go//Remove" + ], + "reaches": [] + }, + { + "key": "os//exec//Cmd", + "definitions": [ + "go:std@go1.25.1//os/exec/exec.go//Cmd" + ], + "reaches": [] + }, + { + "key": "os//exec//Command", + "definitions": [ + "go:std@go1.25.1//os/exec/exec.go//Command" + ], + "reaches": [] + } + ], + "targets": [] +} diff --git a/test/projectgraph/cases/go/interface-method-set/expect.yaml b/test/projectgraph/cases/go/interface-method-set/expect.yaml index 582b0847..0d27bdce 100644 --- a/test/projectgraph/cases/go/interface-method-set/expect.yaml +++ b/test/projectgraph/cases/go/interface-method-set/expect.yaml @@ -1,5 +1,5 @@ source: safedep:hand -description: start calls Run on a parameter of the interface type Runner, which declares Run and Stop. Real has both methods, so it can hold the call. Script has Run with the arity of the call but no Stop, so it is no Runner and its Run gets no cha edge. The site keeps a record with the method Run, and a definition named Run reaches exec.Command, so exec.Command is uncertain and not proved unreached. +description: start calls Run on a parameter of the interface type Runner, which declares Run and Stop. Real has both methods, so it can hold the call. Script has Run with the arity of the call but no Stop, so it is no Runner and its Run gets no cha edge. The site keeps a record with the method Run, and a definition named Run reaches exec.Command, so exec.Command is uncertain and not proved unreached. Script.Run calls Run on the result of exec.Command. No unit gives the code of os/exec, so the site keeps a record with the reason no-dependency-source. roots: - key: cmd/app/main.go kind: main @@ -21,6 +21,10 @@ unresolved: site: cmd/app/main.go:10 reason: no-definition method: Run + - caller: worker/script.go//Script//Run + site: worker/script.go:8 + reason: no-dependency-source + method: Run results: - target: net//http//Get status: found diff --git a/test/projectgraph/cases/javascript/commander-action/expect.yaml b/test/projectgraph/cases/javascript/commander-action/expect.yaml index 23019624..d3fc5b82 100644 --- a/test/projectgraph/cases/javascript/commander-action/expect.yaml +++ b/test/projectgraph/cases/javascript/commander-action/expect.yaml @@ -1,5 +1,5 @@ source: safedep:hand -description: program.command('convert ').action(convert) resolves to a key under commander, so convert is a root of the rule node.commander.action at the call. +description: program.command('convert ').action(convert) resolves to a key under commander, so convert is a root of the rule node.commander.action at the call. The call of action is a call on the result of command, whose type the link step does not know with no unit of commander, so the site keeps a record with the reason no-dependency-source. roots: - key: bin/cli.js//convert kind: framework @@ -11,7 +11,11 @@ edges: kind: direct confidence: high site: bin/cli.js:5 -unresolved: [] +unresolved: + - caller: bin/cli.js + site: bin/cli.js:8 + reason: no-dependency-source + method: action results: - target: js-yaml//load status: found diff --git a/test/projectgraph/cases/javascript/dependency-candidates/summaries/npm_store-kit_1.0.0.json b/test/projectgraph/cases/javascript/dependency-candidates/summaries/npm_store-kit_1.0.0.json index e9fbde93..5e45d1ab 100644 --- a/test/projectgraph/cases/javascript/dependency-candidates/summaries/npm_store-kit_1.0.0.json +++ b/test/projectgraph/cases/javascript/dependency-candidates/summaries/npm_store-kit_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:store-kit@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/dependency-exports-import/summaries/npm_kit_1.0.0.json b/test/projectgraph/cases/javascript/dependency-exports-import/summaries/npm_kit_1.0.0.json index 740f3e2f..c094a670 100644 --- a/test/projectgraph/cases/javascript/dependency-exports-import/summaries/npm_kit_1.0.0.json +++ b/test/projectgraph/cases/javascript/dependency-exports-import/summaries/npm_kit_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:kit@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/dependency-exports-require/summaries/npm_kit_1.0.0.json b/test/projectgraph/cases/javascript/dependency-exports-require/summaries/npm_kit_1.0.0.json index 740f3e2f..c094a670 100644 --- a/test/projectgraph/cases/javascript/dependency-exports-require/summaries/npm_kit_1.0.0.json +++ b/test/projectgraph/cases/javascript/dependency-exports-require/summaries/npm_kit_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:kit@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/dependency-method-name/summaries/npm_plug_1.0.0.json b/test/projectgraph/cases/javascript/dependency-method-name/summaries/npm_plug_1.0.0.json index 81d96fc2..3840a85b 100644 --- a/test/projectgraph/cases/javascript/dependency-method-name/summaries/npm_plug_1.0.0.json +++ b/test/projectgraph/cases/javascript/dependency-method-name/summaries/npm_plug_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:plug@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/dependency-same-path/summaries/npm_util-kit_1.0.0.json b/test/projectgraph/cases/javascript/dependency-same-path/summaries/npm_util-kit_1.0.0.json index 9ad20a06..0ee53d7c 100644 --- a/test/projectgraph/cases/javascript/dependency-same-path/summaries/npm_util-kit_1.0.0.json +++ b/test/projectgraph/cases/javascript/dependency-same-path/summaries/npm_util-kit_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:util-kit@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash._reinterpolate_3.0.0.json b/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash._reinterpolate_3.0.0.json index 08756010..7d16d7d1 100644 --- a/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash._reinterpolate_3.0.0.json +++ b/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash._reinterpolate_3.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:lodash._reinterpolate@3.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash_4.17.21.json b/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash_4.17.21.json index 80b848c8..e177f536 100644 --- a/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash_4.17.21.json +++ b/test/projectgraph/cases/javascript/dependency-transitive/summaries/npm_lodash_4.17.21.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "npm:lodash@4.17.21", "symbols": [ { diff --git a/test/projectgraph/cases/javascript/references-argument/expect.yaml b/test/projectgraph/cases/javascript/references-argument/expect.yaml index 00f048e5..edc1f654 100644 --- a/test/projectgraph/cases/javascript/references-argument/expect.yaml +++ b/test/projectgraph/cases/javascript/references-argument/expect.yaml @@ -1,5 +1,5 @@ source: safedep:hand -description: src/main.js passes the imported load to Array.map and the first-party parse to Promise.then. Each reference makes its function possibly called. +description: src/main.js passes the imported load to Array.map and the first-party parse to Promise.then. Each reference makes its function possibly called. The call of then is a call on the result of Promise.resolve, whose type the link step does not know with no code of Promise, so the site keeps a record with the reason no-dependency-source. roots: - key: src/main.js kind: manifest @@ -15,7 +15,11 @@ edges: kind: reference confidence: low site: src/main.js:5 -unresolved: [] +unresolved: + - caller: src/main.js + site: src/main.js:5 + reason: no-dependency-source + method: then results: - target: js-yaml//load status: found diff --git a/test/projectgraph/cases/python/dependency-dark-unit/summaries/pypi_PyYAML_6.0.1.json b/test/projectgraph/cases/python/dependency-dark-unit/summaries/pypi_PyYAML_6.0.1.json index aa139f6b..5457ea6f 100644 --- a/test/projectgraph/cases/python/dependency-dark-unit/summaries/pypi_PyYAML_6.0.1.json +++ b/test/projectgraph/cases/python/dependency-dark-unit/summaries/pypi_PyYAML_6.0.1.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:PyYAML@6.0.1", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_badpkg_1.0.0.json b/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_badpkg_1.0.0.json index ba9a9dff..fb669126 100644 --- a/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_badpkg_1.0.0.json +++ b/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_badpkg_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:badpkg@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_okpkg_1.0.0.json b/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_okpkg_1.0.0.json index e1ded119..39710708 100644 --- a/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_okpkg_1.0.0.json +++ b/test/projectgraph/cases/python/dependency-failed-reached/summaries/pypi_okpkg_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:okpkg@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_badpkg_1.0.0.json b/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_badpkg_1.0.0.json index ba9a9dff..fb669126 100644 --- a/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_badpkg_1.0.0.json +++ b/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_badpkg_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:badpkg@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_okpkg_1.0.0.json b/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_okpkg_1.0.0.json index e1ded119..39710708 100644 --- a/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_okpkg_1.0.0.json +++ b/test/projectgraph/cases/python/dependency-failed-unreached/summaries/pypi_okpkg_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:okpkg@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-package-dir/summaries/pypi_outer_1.0.0.json b/test/projectgraph/cases/python/dependency-package-dir/summaries/pypi_outer_1.0.0.json index 67b3ee37..060315ed 100644 --- a/test/projectgraph/cases/python/dependency-package-dir/summaries/pypi_outer_1.0.0.json +++ b/test/projectgraph/cases/python/dependency-package-dir/summaries/pypi_outer_1.0.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:outer@1.0.0", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_requests_2.31.0.json b/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_requests_2.31.0.json index 333c730a..064e485d 100644 --- a/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_requests_2.31.0.json +++ b/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_requests_2.31.0.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:requests@2.31.0", "symbols": [ { diff --git a/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_urllib3_2.0.7.json b/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_urllib3_2.0.7.json index e7b25688..567fa307 100644 --- a/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_urllib3_2.0.7.json +++ b/test/projectgraph/cases/python/dependency-site-packages/summaries/pypi_urllib3_2.0.7.json @@ -1,7 +1,7 @@ { "version": "2", - "index_version": "11", - "link_version": "11", + "index_version": "14", + "link_version": "16", "unit": "pypi:urllib3@2.0.7", "symbols": [ { diff --git a/test/projectgraph/cases/rust/dependency-crate/expect.yaml b/test/projectgraph/cases/rust/dependency-crate/expect.yaml new file mode 100644 index 00000000..5d74e145 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/expect.yaml @@ -0,0 +1,170 @@ +source: safedep:hand +description: >- + main opens a kit::Store with Store::open().unwrap() and calls save on it, and flush::all calls flush on a value of + a generic type S: Sink. The code of the crate kit is the unit cargo:kit@1.0.0. open returns Result, + so unwrap gives a Store, and the call of save gets the external key kit//Store//save, which a package edge joins + to the method in the unit. The bound Sink is a trait of the unit, and Store implements it, so flush gets a cha + edge to kit//Store//flush, and the site keeps its record. Store is in a private module, and lib.rs gives it on + with pub use, so its key is kit//Store and not kit//store//Store. +units: + vendor/kit: cargo:kit@1.0.0 +roots: + - key: src/main.rs + kind: main + rule: rust.main + - key: src/main.rs//main + kind: main + rule: rust.main +exact_edges: true +edges: + - from: cargo:kit@1.0.0//src/store.rs//Store//flush + to: cargo:kit@1.0.0//src/store.rs//Store + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:18 + - from: cargo:kit@1.0.0//src/store.rs//Store//flush + to: std//fs//remove_file + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:19 + - from: cargo:kit@1.0.0//src/store.rs//Store//flush + to: std//fs//remove_file//unwrap + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:19 + - from: cargo:kit@1.0.0//src/store.rs//Store//open + to: Ok + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:9 + - from: cargo:kit@1.0.0//src/store.rs//Store//open + to: cargo:kit@1.0.0//src/store.rs//Store + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:9 + - from: cargo:kit@1.0.0//src/store.rs//Store//save + to: cargo:kit@1.0.0//src/store.rs//Store + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:12 + - from: cargo:kit@1.0.0//src/store.rs//Store//save + to: std//fs//write + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:13 + - from: cargo:kit@1.0.0//src/store.rs//Store//save + to: std//fs//write//unwrap + kind: direct + confidence: high + site: cargo:kit@1.0.0//src/store.rs:13 + - from: kit//Store//flush + to: cargo:kit@1.0.0//src/store.rs//Store//flush + kind: package + confidence: high + site: cargo:kit@1.0.0//src/store.rs:18 + - from: kit//Store//open + to: cargo:kit@1.0.0//src/store.rs//Store//open + kind: package + confidence: high + site: cargo:kit@1.0.0//src/store.rs:8 + - from: kit//Store//open//unwrap + to: cargo:kit@1.0.0//src/store.rs//Store//open + kind: package + confidence: high + site: cargo:kit@1.0.0//src/store.rs:8 + - from: kit//Store//save + to: cargo:kit@1.0.0//src/store.rs//Store//save + kind: package + confidence: high + site: cargo:kit@1.0.0//src/store.rs:12 + - from: src/flush.rs//all + to: kit//Store//flush + kind: cha + confidence: medium + site: src/flush.rs:4 + - from: src/main.rs//main + to: kit//Store//open + kind: direct + confidence: high + site: src/main.rs:6 + - from: src/main.rs//main + to: kit//Store//open//unwrap + kind: direct + confidence: high + site: src/main.rs:6 + - from: src/main.rs//main + to: kit//Store//save + kind: direct + confidence: high + site: src/main.rs:7 + - from: src/main.rs//main + to: src/flush.rs//all + kind: direct + confidence: high + site: src/main.rs:8 +unresolved: + - caller: cargo:kit@1.0.0//src/store.rs//Store//save + site: cargo:kit@1.0.0//src/store.rs:13 + reason: unknown-receiver + method: unwrap + - caller: cargo:kit@1.0.0//src/store.rs//Store//flush + site: cargo:kit@1.0.0//src/store.rs:19 + reason: unknown-receiver + method: unwrap + - caller: src/flush.rs//all + site: src/flush.rs:4 + reason: no-definition + method: flush + - caller: src/main.rs//main + site: src/main.rs:6 + reason: unknown-receiver + method: unwrap +results: + - target: kit//Store//save + status: found + path: + - key: src/main.rs//main + - key: kit//Store//save + site: src/main.rs:7 + kind: direct + confidence: high + call_sites: + - caller: src/main.rs//main + site: src/main.rs:7 + reached: true + - target: std//fs//write + status: found + path: + - key: src/main.rs//main + - key: kit//Store//save + site: src/main.rs:7 + kind: direct + confidence: high + - key: cargo:kit@1.0.0//src/store.rs//Store//save + site: cargo:kit@1.0.0//src/store.rs:12 + kind: package + confidence: high + - key: std//fs//write + site: cargo:kit@1.0.0//src/store.rs:13 + kind: direct + confidence: high + call_sites: + - caller: cargo:kit@1.0.0//src/store.rs//Store//save + site: cargo:kit@1.0.0//src/store.rs:13 + reached: true + - target: kit//Store//flush + status: found + path: + - key: src/main.rs//main + - key: src/flush.rs//all + site: src/main.rs:8 + kind: direct + confidence: high + - key: kit//Store//flush + site: src/flush.rs:4 + kind: cha + confidence: medium + call_sites: + - caller: src/flush.rs//all + site: src/flush.rs:4 + reached: true diff --git a/test/projectgraph/cases/rust/dependency-crate/project/Cargo.toml b/test/projectgraph/cases/rust/dependency-crate/project/Cargo.toml new file mode 100644 index 00000000..492d638f --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +[dependencies] +kit = "1.0.0" diff --git a/test/projectgraph/cases/rust/dependency-crate/project/src/flush.rs b/test/projectgraph/cases/rust/dependency-crate/project/src/flush.rs new file mode 100644 index 00000000..24d29799 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/project/src/flush.rs @@ -0,0 +1,5 @@ +use kit::Sink; + +pub fn all(sink: &S) { + sink.flush(); +} diff --git a/test/projectgraph/cases/rust/dependency-crate/project/src/main.rs b/test/projectgraph/cases/rust/dependency-crate/project/src/main.rs new file mode 100644 index 00000000..1e6fa13f --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/project/src/main.rs @@ -0,0 +1,9 @@ +mod flush; + +use kit::Store; + +fn main() { + let store = Store::open().unwrap(); + store.save(); + flush::all(&store); +} diff --git a/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/Cargo.toml b/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/Cargo.toml new file mode 100644 index 00000000..58da5f79 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "kit" +version = "1.0.0" +edition = "2021" diff --git a/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/src/lib.rs b/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/src/lib.rs new file mode 100644 index 00000000..6f15fdc1 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/src/lib.rs @@ -0,0 +1,7 @@ +mod store; + +pub use store::Store; + +pub trait Sink { + fn flush(&self); +} diff --git a/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/src/store.rs b/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/src/store.rs new file mode 100644 index 00000000..83c7fc95 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/project/vendor/kit/src/store.rs @@ -0,0 +1,21 @@ +use crate::Sink; + +pub struct Store { + path: String, +} + +impl Store { + pub fn open() -> Result { + Ok(Store { path: String::new() }) + } + + pub fn save(&self) { + std::fs::write(&self.path, "data").unwrap(); + } +} + +impl Sink for Store { + fn flush(&self) { + std::fs::remove_file(&self.path).unwrap(); + } +} diff --git a/test/projectgraph/cases/rust/dependency-crate/summaries/cargo_kit_1.0.0.json b/test/projectgraph/cases/rust/dependency-crate/summaries/cargo_kit_1.0.0.json new file mode 100644 index 00000000..4bc0b860 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-crate/summaries/cargo_kit_1.0.0.json @@ -0,0 +1,74 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "cargo:kit@1.0.0", + "symbols": [ + { + "key": "kit//Sink", + "definitions": [ + "cargo:kit@1.0.0//src/lib.rs//Sink" + ], + "reaches": [] + }, + { + "key": "kit//Store", + "definitions": [ + "cargo:kit@1.0.0//src/store.rs//Store" + ], + "reaches": [] + }, + { + "key": "kit//Store//flush", + "definitions": [ + "cargo:kit@1.0.0//src/store.rs//Store//flush" + ], + "reaches": [], + "uncertainties": [ + 0 + ] + }, + { + "key": "kit//Store//open", + "definitions": [ + "cargo:kit@1.0.0//src/store.rs//Store//open" + ], + "reaches": [] + }, + { + "key": "kit//Store//save", + "definitions": [ + "cargo:kit@1.0.0//src/store.rs//Store//save" + ], + "reaches": [], + "uncertainties": [ + 1 + ] + } + ], + "targets": [], + "uncertainties": [ + { + "kind": "unresolved-call", + "key": "cargo:kit@1.0.0//src/store.rs//Store//flush", + "site": { + "path": "cargo:kit@1.0.0//src/store.rs", + "line": 19, + "column": 9 + }, + "text": "std::fs::remove_file(\u0026self.path).unwrap (unknown-receiver)", + "method": "unwrap" + }, + { + "kind": "unresolved-call", + "key": "cargo:kit@1.0.0//src/store.rs//Store//save", + "site": { + "path": "cargo:kit@1.0.0//src/store.rs", + "line": 13, + "column": 9 + }, + "text": "std::fs::write(\u0026self.path, \"data\").unwrap (unknown-receiver)", + "method": "unwrap" + } + ] +} diff --git a/test/projectgraph/cases/rust/dependency-internal-call/expect.yaml b/test/projectgraph/cases/rust/dependency-internal-call/expect.yaml new file mode 100644 index 00000000..151d8727 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-internal-call/expect.yaml @@ -0,0 +1,36 @@ +source: safedep:hand +description: >- + main calls serve of the crate kit, the unit cargo:kit@1.0.0. serve calls the private parse_header. No file of the + project names parse_header, so the graph has no external node for it. The target kit//parse_header matches the + definition in the unit by its external key, so reach finds the path through serve. +units: + vendor/kit: cargo:kit@1.0.0 +roots: + - key: src/main.rs + kind: main + rule: rust.main + - key: src/main.rs//main + kind: main + rule: rust.main +unresolved: [] +results: + - target: kit//parse_header + status: found + path: + - key: src/main.rs//main + - key: kit//serve + site: src/main.rs:2 + kind: direct + confidence: high + - key: cargo:kit@1.0.0//src/lib.rs//serve + site: cargo:kit@1.0.0//src/lib.rs:1 + kind: package + confidence: high + - key: cargo:kit@1.0.0//src/lib.rs//parse_header + site: cargo:kit@1.0.0//src/lib.rs:2 + kind: direct + confidence: high + call_sites: + - caller: cargo:kit@1.0.0//src/lib.rs//serve + site: cargo:kit@1.0.0//src/lib.rs:2 + reached: true \ No newline at end of file diff --git a/test/projectgraph/cases/rust/dependency-internal-call/project/Cargo.toml b/test/projectgraph/cases/rust/dependency-internal-call/project/Cargo.toml new file mode 100644 index 00000000..492d638f --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-internal-call/project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +[dependencies] +kit = "1.0.0" diff --git a/test/projectgraph/cases/rust/dependency-internal-call/project/src/main.rs b/test/projectgraph/cases/rust/dependency-internal-call/project/src/main.rs new file mode 100644 index 00000000..0e24e669 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-internal-call/project/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + kit::serve(); +} diff --git a/test/projectgraph/cases/rust/dependency-internal-call/project/vendor/kit/Cargo.toml b/test/projectgraph/cases/rust/dependency-internal-call/project/vendor/kit/Cargo.toml new file mode 100644 index 00000000..58da5f79 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-internal-call/project/vendor/kit/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "kit" +version = "1.0.0" +edition = "2021" diff --git a/test/projectgraph/cases/rust/dependency-internal-call/project/vendor/kit/src/lib.rs b/test/projectgraph/cases/rust/dependency-internal-call/project/vendor/kit/src/lib.rs new file mode 100644 index 00000000..7746db0b --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-internal-call/project/vendor/kit/src/lib.rs @@ -0,0 +1,5 @@ +pub fn serve() { + parse_header(); +} + +fn parse_header() {} diff --git a/test/projectgraph/cases/rust/dependency-internal-call/summaries/cargo_kit_1.0.0.json b/test/projectgraph/cases/rust/dependency-internal-call/summaries/cargo_kit_1.0.0.json new file mode 100644 index 00000000..d7c8d300 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-internal-call/summaries/cargo_kit_1.0.0.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "cargo:kit@1.0.0", + "symbols": [ + { + "key": "kit//serve", + "definitions": [ + "cargo:kit@1.0.0//src/lib.rs//serve" + ], + "reaches": [] + } + ], + "targets": [] +} diff --git a/test/projectgraph/cases/rust/dependency-renamed/expect.yaml b/test/projectgraph/cases/rust/dependency-renamed/expect.yaml new file mode 100644 index 00000000..fae15315 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-renamed/expect.yaml @@ -0,0 +1,36 @@ +source: safedep:hand +description: >- + The Cargo.toml of the project renames the package kit to helper, and main calls helper::serve. The link step + reads the rename, so the import names the crate kit of the unit cargo:kit@1.0.0, and reach finds the private + parse_header through serve. Before the link step read renames, the target was not-found. +units: + vendor/kit: cargo:kit@1.0.0 +roots: + - key: src/main.rs + kind: main + rule: rust.main + - key: src/main.rs//main + kind: main + rule: rust.main +unresolved: [] +results: + - target: kit//parse_header + status: found + path: + - key: src/main.rs//main + - key: helper//serve + site: src/main.rs:2 + kind: direct + confidence: high + - key: cargo:kit@1.0.0//src/lib.rs//serve + site: cargo:kit@1.0.0//src/lib.rs:1 + kind: package + confidence: high + - key: cargo:kit@1.0.0//src/lib.rs//parse_header + site: cargo:kit@1.0.0//src/lib.rs:2 + kind: direct + confidence: high + call_sites: + - caller: cargo:kit@1.0.0//src/lib.rs//serve + site: cargo:kit@1.0.0//src/lib.rs:2 + reached: true diff --git a/test/projectgraph/cases/rust/dependency-renamed/project/Cargo.toml b/test/projectgraph/cases/rust/dependency-renamed/project/Cargo.toml new file mode 100644 index 00000000..1e4dd14a --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-renamed/project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +[dependencies] +helper = { package = "kit", version = "1.0.0" } diff --git a/test/projectgraph/cases/rust/dependency-renamed/project/src/main.rs b/test/projectgraph/cases/rust/dependency-renamed/project/src/main.rs new file mode 100644 index 00000000..42454ac3 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-renamed/project/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + helper::serve(); +} diff --git a/test/projectgraph/cases/rust/dependency-renamed/project/vendor/kit/Cargo.toml b/test/projectgraph/cases/rust/dependency-renamed/project/vendor/kit/Cargo.toml new file mode 100644 index 00000000..58da5f79 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-renamed/project/vendor/kit/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "kit" +version = "1.0.0" +edition = "2021" diff --git a/test/projectgraph/cases/rust/dependency-renamed/project/vendor/kit/src/lib.rs b/test/projectgraph/cases/rust/dependency-renamed/project/vendor/kit/src/lib.rs new file mode 100644 index 00000000..7746db0b --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-renamed/project/vendor/kit/src/lib.rs @@ -0,0 +1,5 @@ +pub fn serve() { + parse_header(); +} + +fn parse_header() {} diff --git a/test/projectgraph/cases/rust/dependency-renamed/summaries/cargo_kit_1.0.0.json b/test/projectgraph/cases/rust/dependency-renamed/summaries/cargo_kit_1.0.0.json new file mode 100644 index 00000000..d7c8d300 --- /dev/null +++ b/test/projectgraph/cases/rust/dependency-renamed/summaries/cargo_kit_1.0.0.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "cargo:kit@1.0.0", + "symbols": [ + { + "key": "kit//serve", + "definitions": [ + "cargo:kit@1.0.0//src/lib.rs//serve" + ], + "reaches": [] + } + ], + "targets": [] +} diff --git a/test/projectgraph/cases/rust/library-build-script/expect.yaml b/test/projectgraph/cases/rust/library-build-script/expect.yaml new file mode 100644 index 00000000..30000d38 --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/expect.yaml @@ -0,0 +1,40 @@ +source: safedep:hand +description: >- + A library crate with a build script. build.rs has a main, and it stays a root. The build script is a helper + program, and the users of the crate call its public items, so the public generate of src/lib.rs is a root of kind + library-surface too. generate calls kit::fill of the unit cargo:kit@1.0.0, so reach finds it. When the build + script turned off the library-surface roots, the answer was not-found. +units: + vendor/kit: cargo:kit@1.0.0 +roots: + - key: build.rs + kind: library-surface + rule: library-surface + - key: build.rs + kind: main + rule: rust.build-script + - key: build.rs//main + kind: main + rule: rust.main + - key: src/lib.rs + kind: library-surface + rule: library-surface + - key: src/lib.rs//generate + kind: library-surface + rule: library-surface +unresolved: [] +results: + - target: kit//fill + status: found + path: + - key: src/lib.rs//generate + - key: kit//fill + site: src/lib.rs:2 + kind: direct + confidence: high + call_sites: + - caller: src/lib.rs//generate + site: src/lib.rs:2 + reached: true + uncertainties: + - kind: root-fallback diff --git a/test/projectgraph/cases/rust/library-build-script/project/Cargo.toml b/test/projectgraph/cases/rust/library-build-script/project/Cargo.toml new file mode 100644 index 00000000..0b195ea2 --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "lib" +version = "0.1.0" +edition = "2021" + +[dependencies] +kit = "1.0.0" diff --git a/test/projectgraph/cases/rust/library-build-script/project/build.rs b/test/projectgraph/cases/rust/library-build-script/project/build.rs new file mode 100644 index 00000000..c2bce4fe --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/project/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test/projectgraph/cases/rust/library-build-script/project/src/lib.rs b/test/projectgraph/cases/rust/library-build-script/project/src/lib.rs new file mode 100644 index 00000000..4c8c7a62 --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/project/src/lib.rs @@ -0,0 +1,3 @@ +pub fn generate() { + kit::fill(); +} diff --git a/test/projectgraph/cases/rust/library-build-script/project/vendor/kit/Cargo.toml b/test/projectgraph/cases/rust/library-build-script/project/vendor/kit/Cargo.toml new file mode 100644 index 00000000..58da5f79 --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/project/vendor/kit/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "kit" +version = "1.0.0" +edition = "2021" diff --git a/test/projectgraph/cases/rust/library-build-script/project/vendor/kit/src/lib.rs b/test/projectgraph/cases/rust/library-build-script/project/vendor/kit/src/lib.rs new file mode 100644 index 00000000..f0212044 --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/project/vendor/kit/src/lib.rs @@ -0,0 +1 @@ +pub fn fill() {} diff --git a/test/projectgraph/cases/rust/library-build-script/summaries/cargo_kit_1.0.0.json b/test/projectgraph/cases/rust/library-build-script/summaries/cargo_kit_1.0.0.json new file mode 100644 index 00000000..aa9775d4 --- /dev/null +++ b/test/projectgraph/cases/rust/library-build-script/summaries/cargo_kit_1.0.0.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "index_version": "14", + "link_version": "16", + "unit": "cargo:kit@1.0.0", + "symbols": [ + { + "key": "kit//fill", + "definitions": [ + "cargo:kit@1.0.0//src/lib.rs//fill" + ], + "reaches": [] + } + ], + "targets": [] +} diff --git a/test/projectgraph/cases/rust/use-crate-name/expect.yaml b/test/projectgraph/cases/rust/use-crate-name/expect.yaml new file mode 100644 index 00000000..113d2081 --- /dev/null +++ b/test/projectgraph/cases/rust/use-crate-name/expect.yaml @@ -0,0 +1,49 @@ +source: safedep:hand +description: >- + main.rs has use smallvec::smallvec, a use whose last name is the name of its crate. Rust resolves the first + smallvec against the crates, not against the binding of the same use, so the link step ends. main calls + store::save, and save calls SmallVec::new of the crate smallvec. No call names the macro smallvec. +roots: + - key: src/main.rs + kind: main + rule: rust.main + - key: src/main.rs//main + kind: main + rule: rust.main +exact_edges: true +edges: + - from: src/main.rs//main + to: src/store.rs//save + kind: direct + confidence: high + site: src/main.rs:6 + - from: src/store.rs//save + to: smallvec//SmallVec + kind: reference + confidence: low + site: src/store.rs:4 + - from: src/store.rs//save + to: smallvec//SmallVec//new + kind: direct + confidence: high + site: src/store.rs:4 +unresolved: [] +results: + - target: smallvec//SmallVec//new + status: found + path: + - key: src/main.rs//main + - key: src/store.rs//save + site: src/main.rs:6 + kind: direct + confidence: high + - key: smallvec//SmallVec//new + site: src/store.rs:4 + kind: direct + confidence: high + call_sites: + - caller: src/store.rs//save + site: src/store.rs:4 + reached: true + - target: smallvec//smallvec + status: no-such-target diff --git a/test/projectgraph/cases/rust/use-crate-name/project/Cargo.toml b/test/projectgraph/cases/rust/use-crate-name/project/Cargo.toml new file mode 100644 index 00000000..dbeadb3d --- /dev/null +++ b/test/projectgraph/cases/rust/use-crate-name/project/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +[dependencies] +smallvec = "1.13" diff --git a/test/projectgraph/cases/rust/use-crate-name/project/src/main.rs b/test/projectgraph/cases/rust/use-crate-name/project/src/main.rs new file mode 100644 index 00000000..68bde29f --- /dev/null +++ b/test/projectgraph/cases/rust/use-crate-name/project/src/main.rs @@ -0,0 +1,7 @@ +use smallvec::smallvec; + +mod store; + +fn main() { + store::save(); +} diff --git a/test/projectgraph/cases/rust/use-crate-name/project/src/store.rs b/test/projectgraph/cases/rust/use-crate-name/project/src/store.rs new file mode 100644 index 00000000..9d51d0e1 --- /dev/null +++ b/test/projectgraph/cases/rust/use-crate-name/project/src/store.rs @@ -0,0 +1,5 @@ +use smallvec::SmallVec; + +pub fn save() { + let _buf: SmallVec<[u8; 4]> = SmallVec::new(); +} diff --git a/test/projectgraph/methods_test.go b/test/projectgraph/methods_test.go new file mode 100644 index 00000000..5b2b5ea7 --- /dev/null +++ b/test/projectgraph/methods_test.go @@ -0,0 +1,290 @@ +//go:build projectgraph + +package projectgraph + +import ( + "bufio" + "context" + "encoding/json" + iofs "io/fs" + "maps" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + "testing" + "unicode" + "unicode/utf8" + + "github.com/safedep/code/fileindex" + codefs "github.com/safedep/code/fs" + "github.com/safedep/code/lang" + "github.com/safedep/code/plugin/indexer" + "github.com/safedep/code/projectgraph" + "github.com/safedep/code/projectgraph/reach" + "github.com/stretchr/testify/require" + "golang.org/x/mod/module" + "golang.org/x/mod/semver" +) + +// methodCorpusEnv names the directory that scripts/fetch_method_corpus.sh +// fills with the projects and the dependencies of the method measure. +const methodCorpusEnv = "PROJECTGRAPH_METHOD_CORPUS" + +// methodsDir holds the list of the projects and the advisories of their +// dependencies, from the Go vulndb and from RustSec through OSV. +var methodsDir = filepath.Join("public", "methods") + +// methodProject is one line of corpus.txt. +type methodProject struct { + language, name, source string + deps []string +} + +// advisory is one advisory of a package: the events of its affected +// ranges, and the symbols of Go or the functions of Rust that it names. +type advisory struct { + ID string `json:"id"` + Ranges []struct { + Introduced string `json:"introduced"` + Fixed string `json:"fixed"` + } `json:"ranges"` + Imports []struct { + Path string `json:"path"` + Symbols []string `json:"symbols"` + } `json:"imports"` + Functions []string `json:"functions"` +} + +// affects reports whether the ranges of the advisory hold the version. +func (a advisory) affects(version string) bool { + v := "v" + strings.TrimPrefix(version, "v") + inside := false + for _, e := range a.Ranges { + switch { + case e.Introduced == "0", e.Introduced != "" && semver.Compare("v"+e.Introduced, v) <= 0: + inside = true + case e.Fixed != "" && semver.Compare("v"+e.Fixed, v) <= 0: + inside = false + } + } + return inside +} + +// methodTarget is a target of the measure and the kind of its symbol. +type methodTarget struct { + key string + kind string +} + +// symbolKinds name the kind of a symbol: a function, a method of a type, +// or another name, as a type that RustSec lists. +const ( + kindFunction = "function" + kindMethod = "method" + kindOther = "other" +) + +func readMethodCorpus(t *testing.T) ([]methodProject, map[string]map[string][]advisory) { + f, err := os.Open(filepath.Join(methodsDir, "corpus.txt")) + require.NoError(t, err) + defer func() { require.NoError(t, f.Close()) }() + var projects []methodProject + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) == 0 || strings.HasPrefix(fields[0], "#") { + continue + } + require.GreaterOrEqual(t, len(fields), 3, scanner.Text()) + projects = append(projects, methodProject{language: fields[0], name: fields[1], source: fields[2], deps: fields[3:]}) + } + require.NoError(t, scanner.Err()) + + data, err := os.ReadFile(filepath.Join(methodsDir, "advisories.json")) + require.NoError(t, err) + var advisories map[string]map[string][]advisory + require.NoError(t, json.Unmarshal(data, &advisories)) + return projects, advisories +} + +// methodTargets returns the symbols that the advisories of a dependency +// name at its version, each once. An advisory with no symbol list covers +// the whole package, which the measure cannot list, and a Rust name with +// type arguments is no key. +func methodTargets(language, dep string, advisories map[string]map[string][]advisory) []methodTarget { + name, version, _ := strings.Cut(dep, "@") + seen := map[string]bool{} + var out []methodTarget + add := func(key, kind string) { + if !seen[key] { + seen[key] = true + out = append(out, methodTarget{key: key, kind: kind}) + } + } + if language == "go" { + for _, a := range advisories["go"][name] { + if !a.affects(version) { + continue + } + for _, imp := range a.Imports { + for _, sym := range imp.Symbols { + kind := kindFunction + if strings.Contains(sym, ".") { + kind = kindMethod + } + add(fileindex.Join(fileindex.Namespace("go", imp.Path), strings.Split(sym, ".")...), kind) + } + } + } + return out + } + for _, a := range advisories["cargo"][name] { + if !a.affects(version) { + continue + } + for _, fn := range a.Functions { + if strings.ContainsAny(fn, "<> ") { + continue + } + segs := strings.Split(fn, "::") + kind := kindFunction + switch { + case len(segs) > 2 && startsUpper(segs[len(segs)-2]): + kind = kindMethod + case startsUpper(segs[len(segs)-1]): + kind = kindOther + } + add(strings.Join(segs, fileindex.Separator), kind) + } + } + return out +} + +func startsUpper(s string) bool { + r, _ := utf8.DecodeRuneInString(s) + return unicode.IsUpper(r) +} + +// methodDir returns the directory of a project or a dependency in the +// corpus, and its unit. +func methodDir(t *testing.T, corpus, language, item string) (string, string) { + name, version, _ := strings.Cut(item, "@") + if language == "go" { + escaped, err := module.EscapePath(name) + require.NoError(t, err) + return filepath.Join(corpus, "gomod", filepath.FromSlash(escaped)+"@"+version), "go:" + item + } + return filepath.Join(corpus, "crates", name+"-"+version), "cargo:" + item +} + +// nonTestRoots returns the keys of the roots of the graph that are not +// test roots, as the search of a query without tests starts from them. +func nonTestRoots(g *projectgraph.Graph) []string { + var keys []string + for _, r := range g.Roots { + if r.Kind != projectgraph.RootTest { + keys = append(keys, r.Key) + } + } + return keys +} + +// methodCounts holds the outcomes of the targets by language and kind. +type methodCounts map[[2]string]map[string]int + +func (c methodCounts) add(language, kind, outcome string) { + k := [2]string{language, kind} + if c[k] == nil { + c[k] = map[string]int{} + } + c[k][outcome]++ +} + +// rows writes one line per language and kind with the count of each +// outcome, and the share of the method targets whose cause is an unknown +// receiver. +func (c methodCounts) rows() []string { + keys := slices.SortedFunc(maps.Keys(c), func(a, b [2]string) int { + return strings.Compare(a[0]+"/"+a[1], b[0]+"/"+b[1]) + }) + var rows []string + for _, k := range keys { + counts := c[k] + total := 0 + for _, n := range counts { + total += n + } + parts := []string{"language=" + k[0], "kind=" + k[1], "targets=" + strconv.Itoa(total)} + for _, o := range slices.Sorted(maps.Keys(counts)) { + parts = append(parts, o+"="+strconv.Itoa(counts[o])) + } + rows = append(rows, strings.Join(parts, " ")) + if k[1] == kindMethod { + rows = append(rows, " unknown-receiver="+ratio{hit: counts["uncertain/"+string(fileindex.UnresolvedUnknownReceiver)], all: total}.String()) + } + } + return rows +} + +// TestMethodTargets measures how often a target of a vulnerable function +// or method of a dependency of a real project ends uncertain. Each target +// is a symbol of an advisory of a dependency at its pinned version, and +// the code of each such dependency is a unit of the link step. +func TestMethodTargets(t *testing.T) { + corpus := os.Getenv(methodCorpusEnv) + if corpus == "" { + t.Skipf("%s is not set: run scripts/fetch_method_corpus.sh", methodCorpusEnv) + } + languages, err := lang.AllLanguages() + require.NoError(t, err) + projects, advisories := readMethodCorpus(t) + + counts := methodCounts{} + var lines []string + for _, p := range projects { + dir, _ := methodDir(t, corpus, p.language, p.source) + cfg := codefs.LocalFileSystemConfig{AppDirectories: []string{dir}, ImportUnits: map[string]string{}, ExcludePatterns: codefs.DefaultSkipPatterns(dir)} + units := map[string]iofs.FS{} + var targets []methodTarget + for _, dep := range p.deps { + depDir, unit := methodDir(t, corpus, p.language, dep) + cfg.ImportDirectories = append(cfg.ImportDirectories, depDir) + cfg.ImportUnits[depDir] = unit + cfg.ExcludePatterns = append(cfg.ExcludePatterns, codefs.SkipPattern(depDir)) + units[unit] = os.DirFS(depDir) + targets = append(targets, methodTargets(p.language, dep, advisories)...) + } + fsys, err := codefs.NewLocalFileSystem(cfg) + require.NoError(t, err) + indexed := indexProject(t, fsys, indexer.Options{Languages: languages, Imports: true}) + g := linkProject(t, dir, indexed, projectgraph.Options{TestPaths: fileindex.DefaultTestPaths, Units: units}) + + keys := make([]reach.Target, 0, len(targets)) + for _, mt := range targets { + target, err := reach.TargetByKey(mt.key) + require.NoError(t, err, mt.key) + keys = append(keys, target) + } + results, err := reach.Reach(context.Background(), g, reach.Query{Targets: keys}) + require.NoError(t, err) + methods := receiverReasons(t, g, nonTestRoots(g)) + project := methodCounts{} + for i, r := range results { + o := outcome(r, methods) + counts.add(p.language, targets[i].kind, o) + project.add(p.language, targets[i].kind, o) + } + lines = append(lines, "project="+p.name+" files="+strconv.Itoa(len(g.Files))+" truncated="+strconv.Itoa(len(g.Truncated))+" problems="+strconv.Itoa(len(g.Problems))) + for _, row := range project.rows() { + lines = append(lines, " "+row) + } + } + rows := append([]string{"set=methods projects=" + strconv.Itoa(len(projects))}, counts.rows()...) + rows = append(rows, lines...) + t.Log("\n" + strings.Join(rows, "\n")) + if out := os.Getenv("PROJECTGRAPH_METHOD_REPORT"); out != "" { + require.NoError(t, os.WriteFile(out, []byte(strings.Join(rows, "\n")+"\n"), 0o644)) + } +} diff --git a/test/projectgraph/public/methods/advisories.json b/test/projectgraph/public/methods/advisories.json new file mode 100644 index 00000000..b8e158a2 --- /dev/null +++ b/test/projectgraph/public/methods/advisories.json @@ -0,0 +1,4296 @@ +{ + "cargo": { + "image": [ + { + "functions": [ + "image::hdr::HDRDecoder::read_image_transform" + ], + "id": "RUSTSEC-2019-0014", + "ranges": [ + { + "introduced": "0.10.2" + }, + { + "fixed": "0.21.3" + } + ] + }, + { + "functions": [ + "image::Bgr::from_slice_mut", + "image::Bgra::from_slice_mut", + "image::Luma::from_slice_mut", + "image::LumaA::from_slice_mut", + "image::Rgb::from_slice_mut", + "image::Rgba::from_slice_mut" + ], + "id": "RUSTSEC-2020-0073", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.23.12" + } + ] + } + ], + "lettre": [ + { + "functions": [ + "lettre::sendmail::SendmailTransport::send", + "lettre::transport::sendmail::SendmailTransport::send", + "lettre::transport::sendmail::SendmailTransport::send_raw" + ], + "id": "RUSTSEC-2020-0069", + "ranges": [ + { + "introduced": "0.7.0" + }, + { + "fixed": "0.7.1" + }, + { + "introduced": "0.8.0" + }, + { + "fixed": "0.8.4" + }, + { + "introduced": "0.9.0" + }, + { + "fixed": "0.9.5" + }, + { + "introduced": "0.10.0-alpha.1" + }, + { + "fixed": "0.10.0-alpha.4" + } + ] + }, + { + "functions": [ + "lettre::smtp::SmtpTransport::send", + "lettre::transport::smtp::SmtpTransport::send", + "lettre::transport::smtp::SmtpTransport::send_raw" + ], + "id": "RUSTSEC-2021-0069", + "ranges": [ + { + "introduced": "0.7.0" + }, + { + "fixed": "0.9.6" + }, + { + "introduced": "0.10.0-alpha.1" + }, + { + "fixed": "0.10.0-rc.3" + } + ] + } + ], + "lock_api": [ + { + "functions": [ + "lock_api::MappedMutexGuard", + "lock_api::MappedRwLockReadGuard", + "lock_api::MappedRwLockWriteGuard", + "lock_api::RwLockReadGuard", + "lock_api::RwLockWriteGuard" + ], + "id": "RUSTSEC-2020-0070", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.4.2" + } + ] + } + ], + "openssl": [ + { + "functions": [ + "openssl::x509::X509NameBuilder::build" + ], + "id": "RUSTSEC-2023-0022", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.48" + } + ] + }, + { + "functions": [ + "openssl::x509::extension::ExtendedKeyUsage::other", + "openssl::x509::extension::SubjectAlternativeName::new" + ], + "id": "RUSTSEC-2023-0023", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.48" + } + ] + }, + { + "functions": [ + "openssl::x509::X509Extension::new", + "openssl::x509::X509Extension::new_nid" + ], + "id": "RUSTSEC-2023-0024", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.48" + } + ] + }, + { + "functions": [ + "openssl::x509::verify::X509VerifyParamRef::set_host" + ], + "id": "RUSTSEC-2023-0044", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.55" + } + ] + }, + { + "functions": [ + "openssl::x509::store::X509StoreRef::objects" + ], + "id": "RUSTSEC-2023-0072", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.60" + } + ] + }, + { + "functions": [ + "openssl::bio::MemBio::get_buf" + ], + "id": "RUSTSEC-2024-0357", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.66" + } + ] + }, + { + "functions": [ + "openssl::ssl::select_next_proto" + ], + "id": "RUSTSEC-2025-0004", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.10.70" + } + ] + }, + { + "functions": [ + "openssl::cipher::Cipher::fetch", + "openssl::md::Md::fetch" + ], + "id": "RUSTSEC-2025-0022", + "ranges": [ + { + "introduced": "0.10.39" + }, + { + "fixed": "0.10.72" + } + ] + } + ], + "postgres-protocol": [ + { + "functions": [ + "postgres_protocol::authentication::sasl::ScramSha256::update" + ], + "id": "RUSTSEC-2026-0179", + "ranges": [ + { + "introduced": "0.3.0" + }, + { + "fixed": "0.6.12" + } + ] + }, + { + "functions": [ + "postgres_protocol::types::hstore_from_sql" + ], + "id": "RUSTSEC-2026-0180", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.6.12" + } + ] + } + ], + "rand": [ + { + "functions": [ + "rand::rng", + "rand::thread_rng" + ], + "id": "RUSTSEC-2026-0097", + "ranges": [ + { + "introduced": "0.7.0" + }, + { + "fixed": "0.8.6" + }, + { + "introduced": "0.9.0" + }, + { + "fixed": "0.9.3" + }, + { + "introduced": "0.10.0" + }, + { + "fixed": "0.10.1" + } + ] + } + ], + "rulinalg": [ + { + "functions": [ + "rulinalg::matrix::RowMut::raw_slice", + "rulinalg::matrix::RowMut::raw_slice_mut" + ], + "id": "RUSTSEC-2020-0023", + "ranges": [ + { + "introduced": "0.4.0" + } + ] + } + ], + "rusqlite": [ + { + "functions": [ + "rusqlite::Connection::get_aux", + "rusqlite::Connection::set_aux", + "rusqlite::session::Session::attach", + "rusqlite::session::Session::diff", + "rusqlite::trace::log", + "rusqlite::vtab::create_module" + ], + "id": "RUSTSEC-2020-0014", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.23.0" + } + ] + }, + { + "functions": [ + "rusqlite::Connection::commit_hook", + "rusqlite::Connection::create_aggregate_function", + "rusqlite::Connection::create_collation", + "rusqlite::Connection::create_scalar_function", + "rusqlite::Connection::create_window_function", + "rusqlite::Connection::rollback_hook", + "rusqlite::Connection::update_hook" + ], + "id": "RUSTSEC-2021-0128", + "ranges": [ + { + "introduced": "0.25.0" + }, + { + "fixed": "0.25.4" + }, + { + "introduced": "0.26.0-0" + }, + { + "fixed": "0.26.2" + } + ] + } + ], + "smallvec": [ + { + "functions": [ + "smallvec::SmallVec::grow" + ], + "id": "RUSTSEC-2019-0009", + "ranges": [ + { + "introduced": "0.6.5" + }, + { + "fixed": "0.6.10" + } + ] + }, + { + "functions": [ + "smallvec::SmallVec::grow" + ], + "id": "RUSTSEC-2019-0012", + "ranges": [ + { + "introduced": "0.6.3" + }, + { + "fixed": "0.6.10" + } + ] + }, + { + "functions": [ + "smallvec::SmallVec::insert_many" + ], + "id": "RUSTSEC-2021-0003", + "ranges": [ + { + "introduced": "0.6.3" + }, + { + "fixed": "0.6.14" + }, + { + "introduced": "1.0.0" + }, + { + "fixed": "1.6.1" + } + ] + } + ], + "time": [ + { + "functions": [ + "time::OffsetDateTime::now_local", + "time::OffsetDateTime::try_now_local", + "time::UtcOffset::current_local_offset", + "time::UtcOffset::local_offset_at", + "time::UtcOffset::try_current_local_offset", + "time::UtcOffset::try_local_offset_at", + "time::at", + "time::at_utc", + "time::now" + ], + "id": "RUSTSEC-2020-0071", + "ranges": [ + { + "introduced": "0.0.0-0" + }, + { + "fixed": "0.2.0" + }, + { + "introduced": "0.2.1-0" + }, + { + "fixed": "0.2.1" + }, + { + "introduced": "0.2.2-0" + }, + { + "fixed": "0.2.2" + }, + { + "introduced": "0.2.3-0" + }, + { + "fixed": "0.2.3" + }, + { + "introduced": "0.2.4-0" + }, + { + "fixed": "0.2.4" + }, + { + "introduced": "0.2.5-0" + }, + { + "fixed": "0.2.5" + }, + { + "introduced": "0.2.6-0" + }, + { + "fixed": "0.2.6" + }, + { + "introduced": "0.2.7-0" + }, + { + "fixed": "0.2.23" + } + ] + }, + { + "functions": [ + "time::Date::parse", + "time::OffsetDateTime::parse", + "time::PrimitiveDateTime::parse", + "time::Time::parse", + "time::UtcDateTime::parse", + "time::UtcOffset::parse", + "time::parsing::Parsed::parse_item" + ], + "id": "RUSTSEC-2026-0009", + "ranges": [ + { + "introduced": "0.3.6" + }, + { + "fixed": "0.3.47" + } + ] + } + ], + "tokio": [ + { + "functions": [ + "tokio::task::JoinHandle::abort" + ], + "id": "RUSTSEC-2021-0072", + "ranges": [ + { + "introduced": "0.3.0" + }, + { + "fixed": "1.5.1" + }, + { + "introduced": "1.6.0" + }, + { + "fixed": "1.6.3" + }, + { + "introduced": "1.7.0" + }, + { + "fixed": "1.7.2" + }, + { + "introduced": "1.8.0" + }, + { + "fixed": "1.8.1" + } + ] + }, + { + "functions": [ + "tokio::sync::oneshot::Receiver::close" + ], + "id": "RUSTSEC-2021-0124", + "ranges": [ + { + "introduced": "0.1.14" + }, + { + "fixed": "1.8.4" + }, + { + "introduced": "1.9.0" + }, + { + "fixed": "1.13.1" + } + ] + } + ], + "tokio-postgres": [ + { + "functions": [ + "tokio_postgres::Row::get", + "tokio_postgres::Row::try_get", + "tokio_postgres::SimpleQueryRow::get", + "tokio_postgres::SimpleQueryRow::try_get" + ], + "id": "RUSTSEC-2026-0178", + "ranges": [ + { + "introduced": "0.4.0" + }, + { + "fixed": "0.7.18" + } + ] + } + ] + }, + "go": { + "github.com/antchfx/xmlquery": [ + { + "id": "GO-2020-0048", + "imports": [ + { + "path": "github.com/antchfx/xmlquery", + "symbols": [ + "LoadURL" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.3.1" + } + ] + } + ], + "github.com/antchfx/xpath": [ + { + "id": "GO-2026-4526", + "imports": [ + { + "path": "github.com/antchfx/xpath", + "symbols": [ + "Expr.Evaluate", + "NodeIterator.MoveNext", + "ancestorQuery.Evaluate", + "ancestorQuery.Select", + "attributeQuery.Evaluate", + "attributeQuery.Select", + "booleanQuery.Evaluate", + "booleanQuery.Select", + "cachedChildQuery.Evaluate", + "cachedChildQuery.Select", + "childQuery.Evaluate", + "childQuery.Select", + "descendantOverDescendantQuery.Evaluate", + "descendantOverDescendantQuery.Select", + "descendantQuery.Evaluate", + "descendantQuery.Select", + "filterQuery.Evaluate", + "filterQuery.Select", + "followingQuery.Evaluate", + "followingQuery.Select", + "functionQuery.Evaluate", + "groupQuery.Evaluate", + "groupQuery.Select", + "lastFuncQuery.Evaluate", + "logicalQuery.Evaluate", + "logicalQuery.Select", + "mergeQuery.Evaluate", + "mergeQuery.Select", + "numericQuery.Evaluate", + "parentQuery.Evaluate", + "parentQuery.Select", + "precedingQuery.Evaluate", + "precedingQuery.Select", + "selfQuery.Evaluate", + "selfQuery.Select", + "transformFunctionQuery.Evaluate", + "transformFunctionQuery.Select", + "unionQuery.Evaluate", + "unionQuery.Select" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.3.6" + } + ] + } + ], + "github.com/gorilla/websocket": [ + { + "id": "GO-2020-0019", + "imports": [ + { + "path": "github.com/gorilla/websocket", + "symbols": [ + "Conn.Close", + "Conn.NextReader", + "Conn.NextWriter", + "Conn.ReadJSON", + "Conn.ReadMessage", + "Conn.WriteControl", + "Conn.WriteJSON", + "Conn.WriteMessage", + "Conn.WritePreparedMessage", + "Conn.advanceFrame", + "Dialer.Dial", + "Dialer.DialContext", + "NewClient", + "NewPreparedMessage", + "ReadJSON", + "Subprotocols", + "Upgrade", + "Upgrader.Upgrade", + "WriteJSON", + "flateReadWrapper.Read", + "flateWriteWrapper.Close", + "flateWriteWrapper.Write", + "httpProxyDialer.Dial", + "messageReader.Read", + "messageWriter.Close", + "messageWriter.ReadFrom", + "messageWriter.Write", + "messageWriter.WriteString", + "netDialerFunc.Dial", + "proxy_direct.Dial", + "proxy_envOnce.Get", + "proxy_socks5.Dial", + "truncWriter.Write" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.4.1" + } + ] + }, + { + "id": "GO-2026-6278", + "imports": [ + { + "path": "github.com/gorilla/websocket", + "symbols": [ + "Conn.Close", + "Conn.NextReader", + "Conn.NextWriter", + "Conn.ReadJSON", + "Conn.ReadMessage", + "Conn.WriteControl", + "Conn.WriteJSON", + "Conn.WriteMessage", + "Conn.WritePreparedMessage", + "Dialer.Dial", + "Dialer.DialContext", + "NewClient", + "NewPreparedMessage", + "ReadJSON", + "Subprotocols", + "Upgrade", + "Upgrader.Upgrade", + "WriteJSON", + "flateReadWrapper.Read", + "flateWriteWrapper.Close", + "flateWriteWrapper.Write", + "httpProxyDialer.Dial", + "joinReader.Read", + "messageReader.Read", + "messageWriter.Close", + "messageWriter.ReadFrom", + "messageWriter.Write", + "messageWriter.WriteString", + "netDialerFunc.Dial", + "newMaskKey", + "proxy_direct.Dial", + "proxy_envOnce.Get", + "proxy_socks5.Dial", + "truncWriter.Write" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.5.3" + } + ] + } + ], + "github.com/jackc/pgx/v5": [ + { + "id": "GO-2024-2567", + "imports": [ + { + "path": "github.com/jackc/pgx/v5/pgconn", + "symbols": [ + "Pipeline.Sync" + ] + } + ], + "ranges": [ + { + "introduced": "5.0.0-alpha.5" + }, + { + "fixed": "5.5.2" + } + ] + }, + { + "id": "GO-2024-2606", + "imports": [ + { + "path": "github.com/jackc/pgx/v5/internal/sanitize", + "symbols": [ + "Query.Sanitize", + "SanitizeSQL" + ] + }, + { + "path": "github.com/jackc/pgx/v5/pgproto3", + "symbols": [ + "AuthenticationCleartextPassword.Encode", + "AuthenticationGSS.Encode", + "AuthenticationGSSContinue.Encode", + "AuthenticationMD5Password.Encode", + "AuthenticationOk.Encode", + "AuthenticationSASL.Encode", + "AuthenticationSASLContinue.Encode", + "AuthenticationSASLFinal.Encode", + "Backend.Flush", + "Backend.Send", + "BackendKeyData.Encode", + "Bind.Encode", + "BindComplete.Encode", + "CancelRequest.Encode", + "Close.Encode", + "CloseComplete.Encode", + "CommandComplete.Encode", + "CopyBothResponse.Encode", + "CopyData.Encode", + "CopyDone.Encode", + "CopyFail.Encode", + "CopyInResponse.Encode", + "CopyOutResponse.Encode", + "DataRow.Encode", + "Describe.Encode", + "EmptyQueryResponse.Encode", + "ErrorResponse.Encode", + "ErrorResponse.marshalBinary", + "Execute.Encode", + "Flush.Encode", + "Frontend.Flush", + "Frontend.Send", + "Frontend.SendBind", + "Frontend.SendClose", + "Frontend.SendDescribe", + "Frontend.SendExecute", + "Frontend.SendParse", + "Frontend.SendQuery", + "Frontend.SendSync", + "Frontend.SendUnbufferedEncodedCopyData", + "FunctionCall.Encode", + "FunctionCallResponse.Encode", + "GSSEncRequest.Encode", + "GSSResponse.Encode", + "NoData.Encode", + "NoticeResponse.Encode", + "NotificationResponse.Encode", + "ParameterDescription.Encode", + "ParameterStatus.Encode", + "Parse.Encode", + "ParseComplete.Encode", + "PasswordMessage.Encode", + "PortalSuspended.Encode", + "Query.Encode", + "ReadyForQuery.Encode", + "RowDescription.Encode", + "SASLInitialResponse.Encode", + "SASLResponse.Encode", + "SSLRequest.Encode", + "StartupMessage.Encode", + "Sync.Encode", + "Terminate.Encode" + ] + }, + { + "path": "github.com/jackc/pgx/v5/pgconn", + "symbols": [ + "Batch.ExecParams", + "Batch.ExecPrepared", + "Connect", + "ConnectConfig", + "ConnectWithOptions", + "MultiResultReader.Close", + "MultiResultReader.NextResult", + "MultiResultReader.ReadAll", + "PgConn.CheckConn", + "PgConn.Close", + "PgConn.CopyFrom", + "PgConn.CopyTo", + "PgConn.Deallocate", + "PgConn.Exec", + "PgConn.ExecBatch", + "PgConn.ExecParams", + "PgConn.ExecPrepared", + "PgConn.Ping", + "PgConn.Prepare", + "PgConn.ReceiveMessage", + "PgConn.SyncConn", + "PgConn.WaitForNotification", + "Pipeline.Close", + "Pipeline.Flush", + "Pipeline.GetResults", + "Pipeline.SendDeallocate", + "Pipeline.SendPrepare", + "Pipeline.SendQueryParams", + "Pipeline.SendQueryPrepared", + "Pipeline.Sync", + "ResultReader.Close", + "ResultReader.NextRow", + "ResultReader.Read", + "ValidateConnectTargetSessionAttrsPreferStandby", + "ValidateConnectTargetSessionAttrsPrimary", + "ValidateConnectTargetSessionAttrsReadOnly", + "ValidateConnectTargetSessionAttrsReadWrite", + "ValidateConnectTargetSessionAttrsStandby" + ] + }, + { + "path": "github.com/jackc/pgx/v5/pgproto3/example/pgfortune", + "symbols": [ + "PgFortuneBackend.Run", + "PgFortuneBackend.handleStartup", + "main" + ] + } + ], + "ranges": [ + { + "introduced": "5.0.0" + }, + { + "fixed": "5.5.4" + } + ] + }, + { + "id": "GO-2026-4771", + "imports": [ + { + "path": "github.com/jackc/pgx/v5/pgproto3", + "symbols": [ + "Backend.Receive", + "Bind.Decode" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "5.9.0" + } + ] + }, + { + "id": "GO-2026-4772", + "imports": [ + { + "path": "github.com/jackc/pgx/v5/pgproto3", + "symbols": [ + "Backend.Receive", + "FunctionCall.Decode" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "5.9.0" + } + ] + }, + { + "id": "GO-2026-5004", + "imports": [ + { + "path": "github.com/jackc/pgx/v5/internal/sanitize", + "symbols": [ + "Query.Sanitize", + "SanitizeSQL" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "5.9.2" + } + ] + } + ], + "github.com/rabbitmq/amqp091-go": [ + { + "id": "GO-2026-6372", + "imports": [], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6491", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Connection.Reconnect", + "Connection.openComplete", + "DefaultConnectionRecovery.OnConnectionClose", + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "Open" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6492", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "tlsConfigFromURI" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6493", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "URI.String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6494", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Connection.Reconnect", + "DefaultConnectionRecovery.OnConnectionClose", + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "Open", + "readField", + "reader.ReadFrame" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6496", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Connection.Reconnect", + "DefaultConnectionRecovery.OnConnectionClose", + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "Open", + "readLongstr", + "reader.ReadFrame" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6498", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "AMQPlainAuth.Response", + "Channel.Ack", + "Channel.Cancel", + "Channel.Close", + "Channel.Confirm", + "Channel.Consume", + "Channel.ConsumeWithContext", + "Channel.ExchangeBind", + "Channel.ExchangeDeclare", + "Channel.ExchangeDeclarePassive", + "Channel.ExchangeDelete", + "Channel.ExchangeUnbind", + "Channel.Flow", + "Channel.Get", + "Channel.Nack", + "Channel.Publish", + "Channel.PublishWithContext", + "Channel.PublishWithDeferredConfirm", + "Channel.PublishWithDeferredConfirmWithContext", + "Channel.Qos", + "Channel.QueueBind", + "Channel.QueueDeclare", + "Channel.QueueDeclarePassive", + "Channel.QueueDelete", + "Channel.QueueInspect", + "Channel.QueuePurge", + "Channel.QueueUnbind", + "Channel.Reconnect", + "Channel.Recover", + "Channel.Reject", + "Channel.Tx", + "Channel.TxCommit", + "Channel.TxRollback", + "Connection.Channel", + "Connection.Close", + "Connection.CloseDeadline", + "Connection.Reconnect", + "Connection.UpdateSecret", + "DefaultConnectionRecovery.OnChannelClose", + "DefaultConnectionRecovery.OnConnectionClose", + "Delivery.Ack", + "Delivery.Nack", + "Delivery.Reject", + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "Open", + "writeShortstr", + "writer.WriteFrame", + "writer.WriteFrameNoFlush" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6500", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Channel.recvContent", + "Connection.Reconnect", + "DefaultConnectionRecovery.OnConnectionClose", + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "Open" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6502", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Channel.Qos" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + }, + { + "id": "GO-2026-6503", + "imports": [ + { + "path": "github.com/rabbitmq/amqp091-go", + "symbols": [ + "Connection.Reconnect", + "Connection.openTune", + "DefaultConnectionRecovery.OnConnectionClose", + "Dial", + "DialConfig", + "DialTLS", + "DialTLS_ExternalAuth", + "Open" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.13.0" + } + ] + } + ], + "github.com/sirupsen/logrus": [ + { + "id": "GO-2025-4188", + "imports": [ + { + "path": "github.com/sirupsen/logrus", + "symbols": [ + "Entry.Writer", + "Entry.WriterLevel", + "Entry.writerScanner", + "Logger.Writer", + "Logger.WriterLevel" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.8.3" + }, + { + "introduced": "1.9.0" + }, + { + "fixed": "1.9.1" + }, + { + "introduced": "1.9.2" + }, + { + "fixed": "1.9.3" + } + ] + } + ], + "github.com/valyala/fasthttp": [ + { + "id": "GO-2022-0355", + "imports": [ + { + "path": "github.com/valyala/fasthttp", + "symbols": [ + "AppendBrotliBytes", + "AppendBrotliBytesLevel", + "AppendDeflateBytes", + "AppendDeflateBytesLevel", + "AppendGunzipBytes", + "AppendGzipBytes", + "AppendGzipBytesLevel", + "AppendHTTPDate", + "AppendInflateBytes", + "AppendUnbrotliBytes", + "Args.WriteTo", + "Client.CloseIdleConnections", + "Client.Do", + "Client.DoDeadline", + "Client.DoRedirects", + "Client.DoTimeout", + "Client.Get", + "Client.GetDeadline", + "Client.GetTimeout", + "Client.Post", + "Cookie.AppendBytes", + "Cookie.Cookie", + "Cookie.Parse", + "Cookie.ParseBytes", + "Cookie.String", + "Cookie.WriteTo", + "Dial", + "DialDualStack", + "DialDualStackTimeout", + "DialTimeout", + "Do", + "DoDeadline", + "DoRedirects", + "DoTimeout", + "FS.NewRequestHandler", + "FSHandler", + "FileLastModified", + "GenerateTestCertificate", + "Get", + "GetDeadline", + "GetTimeout", + "HostClient.CloseIdleConnections", + "HostClient.Do", + "HostClient.DoDeadline", + "HostClient.DoRedirects", + "HostClient.DoTimeout", + "HostClient.Get", + "HostClient.GetDeadline", + "HostClient.GetTimeout", + "HostClient.Post", + "LBClient.Do", + "LBClient.DoDeadline", + "LBClient.DoTimeout", + "ListenAndServe", + "ListenAndServeTLS", + "ListenAndServeTLSEmbed", + "ListenAndServeUNIX", + "NewStreamReader", + "ParseByteRange", + "ParseHTTPDate", + "ParseIPv4", + "PipelineClient.Do", + "PipelineClient.DoDeadline", + "PipelineClient.DoTimeout", + "PipelineClient.PendingRequests", + "Post", + "Request.Body", + "Request.BodyGunzip", + "Request.BodyInflate", + "Request.BodyUnbrotli", + "Request.BodyWriteTo", + "Request.ContinueReadBody", + "Request.ContinueReadBodyStream", + "Request.Host", + "Request.MultipartForm", + "Request.PostArgs", + "Request.Read", + "Request.ReadBody", + "Request.ReadLimitBody", + "Request.SetBodyStreamWriter", + "Request.SetHost", + "Request.SetHostBytes", + "Request.String", + "Request.SwapBody", + "Request.URI", + "Request.Write", + "Request.WriteTo", + "RequestCtx.FormFile", + "RequestCtx.FormValue", + "RequestCtx.Host", + "RequestCtx.IfModifiedSince", + "RequestCtx.MultipartForm", + "RequestCtx.Path", + "RequestCtx.PostArgs", + "RequestCtx.PostBody", + "RequestCtx.QueryArgs", + "RequestCtx.Redirect", + "RequestCtx.RedirectBytes", + "RequestCtx.SendFile", + "RequestCtx.SendFileBytes", + "RequestCtx.SetBodyStreamWriter", + "RequestCtx.String", + "RequestCtx.URI", + "RequestHeader.Add", + "RequestHeader.AddBytesK", + "RequestHeader.AddBytesKV", + "RequestHeader.AddBytesV", + "RequestHeader.Read", + "RequestHeader.ReadTrailer", + "RequestHeader.Set", + "RequestHeader.SetByteRange", + "RequestHeader.SetBytesK", + "RequestHeader.SetBytesKV", + "RequestHeader.SetBytesV", + "RequestHeader.SetCanonical", + "RequestHeader.SetReferer", + "RequestHeader.SetRefererBytes", + "RequestHeader.Write", + "Response.Body", + "Response.BodyGunzip", + "Response.BodyInflate", + "Response.BodyUnbrotli", + "Response.BodyWriteTo", + "Response.Read", + "Response.ReadBody", + "Response.ReadLimitBody", + "Response.SendFile", + "Response.SetBodyStreamWriter", + "Response.String", + "Response.SwapBody", + "Response.Write", + "Response.WriteDeflate", + "Response.WriteDeflateLevel", + "Response.WriteGzip", + "Response.WriteGzipLevel", + "Response.WriteTo", + "ResponseHeader.Add", + "ResponseHeader.AddBytesK", + "ResponseHeader.AddBytesKV", + "ResponseHeader.AddBytesV", + "ResponseHeader.AppendBytes", + "ResponseHeader.Cookie", + "ResponseHeader.DelClientCookie", + "ResponseHeader.DelClientCookieBytes", + "ResponseHeader.Header", + "ResponseHeader.Read", + "ResponseHeader.ReadTrailer", + "ResponseHeader.Set", + "ResponseHeader.SetBytesK", + "ResponseHeader.SetBytesKV", + "ResponseHeader.SetBytesV", + "ResponseHeader.SetCanonical", + "ResponseHeader.SetContentRange", + "ResponseHeader.SetCookie", + "ResponseHeader.SetLastModified", + "ResponseHeader.String", + "ResponseHeader.Write", + "ResponseHeader.WriteTo", + "SaveMultipartFile", + "Serve", + "ServeConn", + "ServeFile", + "ServeFileBytes", + "ServeFileBytesUncompressed", + "ServeFileUncompressed", + "ServeTLS", + "ServeTLSEmbed", + "Server.AppendCert", + "Server.AppendCertEmbed", + "Server.ListenAndServe", + "Server.ListenAndServeTLS", + "Server.ListenAndServeTLSEmbed", + "Server.ListenAndServeUNIX", + "Server.Serve", + "Server.ServeConn", + "Server.ServeTLS", + "Server.ServeTLSEmbed", + "Server.Shutdown", + "TCPDialer.Dial", + "TCPDialer.DialDualStack", + "TCPDialer.DialDualStackTimeout", + "TCPDialer.DialTimeout", + "URI.Parse", + "URI.Update", + "URI.UpdateBytes", + "URI.WriteTo", + "WriteBrotli", + "WriteBrotliLevel", + "WriteDeflate", + "WriteDeflateLevel", + "WriteGunzip", + "WriteGzip", + "WriteGzipLevel", + "WriteInflate", + "WriteMultipartForm", + "WriteUnbrotli", + "bigFileReader.Read", + "bigFileReader.WriteTo", + "ctxLogger.Printf", + "firstByteReader.Read", + "flushWriter.Write", + "fsFile.NewReader", + "fsSmallFileReader.WriteTo", + "hijackConn.Close", + "hijackConn.Read", + "perIPConn.Close", + "perIPConnCounter.Unregister", + "pipelineConnClient.Do", + "pipelineConnClient.DoDeadline", + "pipelineConnClient.PendingRequests", + "requestStream.Read", + "statsWriter.Write", + "tcpKeepaliveListener.Accept", + "workerPool.Serve" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.34.0" + } + ] + }, + { + "id": "GO-2026-4950", + "imports": [ + { + "path": "github.com/valyala/fasthttp", + "symbols": [ + "RequestCtx.SendFile", + "RequestCtx.SendFileBytes", + "ServeFS", + "ServeFile", + "ServeFileBytes", + "ServeFileBytesUncompressed", + "ServeFileUncompressed" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.70.0" + } + ] + } + ], + "golang.org/x/crypto": [ + { + "id": "GO-2020-0012", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "CertChecker.Authenticate", + "CertChecker.CheckCert", + "CertChecker.CheckHostKey", + "Certificate.Verify", + "Dial", + "NewClientConn", + "NewPublicKey", + "NewServerConn", + "NewSignerFromKey", + "NewSignerFromSigner", + "ParseAuthorizedKey", + "ParseKnownHosts", + "ParsePrivateKey", + "ParsePrivateKeyWithPassphrase", + "ParsePublicKey", + "ParseRawPrivateKey", + "ParseRawPrivateKeyWithPassphrase", + "ed25519PublicKey.Verify", + "parseED25519", + "parseSKEd25519", + "skEd25519PublicKey.Verify" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20200220183623-bac4c82f6975" + } + ] + }, + { + "id": "GO-2020-0013", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20170330155735-e4e2799dd7aa" + } + ] + }, + { + "id": "GO-2021-0227", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "NewServerConn", + "connection.serverAuthenticate" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20201216223049-8b5274cf687f" + } + ] + }, + { + "id": "GO-2021-0356", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "ServerConfig.AddHostKey" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20220314234659-1baeb1ce4c0b" + } + ] + }, + { + "id": "GO-2022-0209", + "imports": [ + { + "path": "golang.org/x/crypto/salsa20/salsa", + "symbols": [ + "XORKeyStream" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20190320223903-b7391e95e576" + } + ] + }, + { + "id": "GO-2022-0229", + "imports": [ + { + "path": "golang.org/x/crypto/cryptobyte", + "symbols": [] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20200124225646-8b5121be2f68" + } + ] + }, + { + "id": "GO-2022-0968", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn", + "NewServerConn", + "chacha20Poly1305Cipher.readCipherPacket", + "curve25519sha256.Client", + "curve25519sha256.Server", + "dhGEXSHA.Client", + "dhGEXSHA.Server", + "dhGroup.Client", + "dhGroup.Server", + "ecdh.Client", + "ecdh.Server", + "gcmCipher.readCipherPacket" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20211202192323-5770296d904e" + } + ] + }, + { + "id": "GO-2023-1992", + "imports": [ + { + "path": "golang.org/x/crypto/openpgp/clearsign", + "symbols": [ + "Decode" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20190424203555-c05e17bb3b2d" + } + ] + }, + { + "id": "GO-2023-2402", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Client.Dial", + "Client.DialContext", + "Client.DialTCP", + "Client.Listen", + "Client.ListenTCP", + "Client.ListenUnix", + "Client.NewSession", + "Dial", + "DiscardRequests", + "NewClient", + "NewClientConn", + "NewServerConn", + "Request.Reply", + "Session.Close", + "Session.CombinedOutput", + "Session.Output", + "Session.RequestPty", + "Session.RequestSubsystem", + "Session.Run", + "Session.SendRequest", + "Session.Setenv", + "Session.Shell", + "Session.Signal", + "Session.Start", + "Session.WindowChange", + "channel.Accept", + "channel.Close", + "channel.CloseWrite", + "channel.Read", + "channel.ReadExtended", + "channel.Reject", + "channel.SendRequest", + "channel.Write", + "channel.WriteExtended", + "connectionState.readPacket", + "connectionState.writePacket", + "curve25519sha256.Client", + "curve25519sha256.Server", + "dhGEXSHA.Client", + "dhGEXSHA.Server", + "dhGroup.Client", + "dhGroup.Server", + "ecdh.Client", + "ecdh.Server", + "extChannel.Read", + "extChannel.Write", + "handshakeTransport.enterKeyExchange", + "handshakeTransport.readLoop", + "handshakeTransport.sendKexInit", + "mux.OpenChannel", + "mux.SendRequest", + "sessionStdin.Close", + "sshClientKeyboardInteractive.Challenge", + "tcpListener.Accept", + "tcpListener.Close", + "transport.readPacket", + "transport.writePacket", + "unixListener.Accept", + "unixListener.Close" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.17.0" + } + ] + }, + { + "id": "GO-2024-2961", + "imports": [ + { + "path": "golang.org/x/crypto/acme/autocert", + "symbols": [ + "DirCache.Delete", + "DirCache.Get", + "DirCache.Put", + "HostWhitelist", + "Manager.GetCertificate", + "Manager.Listener", + "NewListener", + "listener.Accept", + "listener.Close" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20220525230936-793ad666bf5e" + } + ] + }, + { + "id": "GO-2024-3321", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "NewServerConn", + "ServerConfig.PublicKeyCallback", + "connection.serverAuthenticate" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.31.0" + } + ] + }, + { + "id": "GO-2025-3487", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Client.Dial", + "Client.DialContext", + "Client.DialTCP", + "Client.Listen", + "Client.ListenTCP", + "Client.ListenUnix", + "Client.NewSession", + "Dial", + "DiscardRequests", + "NewClient", + "NewClientConn", + "NewServerConn", + "Request.Reply", + "Session.Close", + "Session.CombinedOutput", + "Session.Output", + "Session.RequestPty", + "Session.RequestSubsystem", + "Session.Run", + "Session.SendRequest", + "Session.Setenv", + "Session.Shell", + "Session.Signal", + "Session.Start", + "Session.WindowChange", + "channel.Accept", + "channel.Close", + "channel.CloseWrite", + "channel.Read", + "channel.ReadExtended", + "channel.Reject", + "channel.SendRequest", + "channel.Write", + "channel.WriteExtended", + "connection.SendAuthBanner", + "curve25519sha256.Client", + "curve25519sha256.Server", + "dhGEXSHA.Client", + "dhGEXSHA.Server", + "dhGroup.Client", + "dhGroup.Server", + "ecdh.Client", + "ecdh.Server", + "extChannel.Read", + "extChannel.Write", + "handshakeTransport.kexLoop", + "handshakeTransport.recordWriteError", + "handshakeTransport.writePacket", + "mux.OpenChannel", + "mux.SendRequest", + "newHandshakeTransport", + "sessionStdin.Close", + "sshClientKeyboardInteractive.Challenge", + "tcpListener.Accept", + "tcpListener.Close", + "unixListener.Accept", + "unixListener.Close" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.35.0" + } + ] + }, + { + "id": "GO-2025-4116", + "imports": [ + { + "path": "golang.org/x/crypto/ssh/agent", + "symbols": [ + "agentKeyringSigner.Sign", + "agentKeyringSigner.SignWithAlgorithm", + "client.List", + "client.Sign", + "client.SignWithFlags", + "client.Signers" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.43.0" + } + ] + }, + { + "id": "GO-2025-4134", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "NewServerConn", + "parseGSSAPIPayload" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.45.0" + } + ] + }, + { + "id": "GO-2025-4135", + "imports": [ + { + "path": "golang.org/x/crypto/ssh/agent", + "symbols": [ + "ForwardToAgent", + "ServeAgent", + "parseConstraints" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.45.0" + } + ] + }, + { + "id": "GO-2026-5005", + "imports": [ + { + "path": "golang.org/x/crypto/ssh/agent", + "symbols": [ + "keyring.Add" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5006", + "imports": [ + { + "path": "golang.org/x/crypto/ssh/agent", + "symbols": [ + "client.Add", + "keyring.Add" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5013", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn", + "NewServerConn", + "curve25519sha256.Client", + "curve25519sha256.Server", + "dhGEXSHA.Client", + "dhGEXSHA.Server", + "dhGroup.Client", + "dhGroup.Server", + "ecdh.Client", + "ecdh.Server", + "gcmCipher.readCipherPacket", + "mlkem768WithCurve25519sha256.Client", + "mlkem768WithCurve25519sha256.Server" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5014", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "NewServerConn", + "connection.serverAuthenticate" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5015", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "CertChecker.Authenticate", + "CertChecker.CheckHostKey" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5016", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "channel.Reject" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5017", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Client.Listen", + "Client.ListenTCP", + "Client.ListenUnix", + "Dial", + "NewClientConn", + "NewServerConn", + "Session.CombinedOutput", + "Session.Output", + "Session.RequestPty", + "Session.RequestSubsystem", + "Session.Run", + "Session.SendRequest", + "Session.Setenv", + "Session.Shell", + "Session.Signal", + "Session.Start", + "Session.WindowChange", + "channel.SendRequest", + "channel.handlePacket", + "mux.SendRequest", + "mux.handleGlobalPacket", + "tcpListener.Close", + "unixListener.Close" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5018", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn", + "NewServerConn", + "NewSignerFromKey", + "ParseAuthorizedKey", + "ParseKnownHosts", + "ParsePrivateKey", + "ParsePrivateKeyWithPassphrase", + "ParsePublicKey", + "ParseRawPrivateKey", + "ParseRawPrivateKeyWithPassphrase", + "checkDSAParams", + "parseDSA", + "parseRSA" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5019", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "CertChecker.Authenticate", + "CertChecker.CheckCert", + "CertChecker.CheckHostKey", + "Certificate.Verify", + "Dial", + "NewClientConn", + "NewServerConn", + "connection.serverAuthenticate", + "skECDSAPublicKey.Verify", + "skEd25519PublicKey.Verify" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5020", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn", + "NewServerConn", + "Session.CombinedOutput", + "Session.Output", + "Session.Run", + "Session.Shell", + "Session.Start", + "channel.Write", + "channel.WriteExtended", + "curve25519sha256.Client", + "curve25519sha256.Server", + "dhGEXSHA.Client", + "dhGEXSHA.Server", + "dhGroup.Client", + "dhGroup.Server", + "ecdh.Client", + "ecdh.Server", + "extChannel.Write", + "mlkem768WithCurve25519sha256.Client", + "mlkem768WithCurve25519sha256.Server" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5021", + "imports": [ + { + "path": "golang.org/x/crypto/ssh/knownhosts", + "symbols": [ + "hostKeyDB.IsRevoked" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5023", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "NewServerConn", + "connection.serverAuthenticate" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5033", + "imports": [ + { + "path": "golang.org/x/crypto/ssh/agent", + "symbols": [ + "ForwardToAgent", + "ServeAgent", + "parseEd25519Cert", + "parseEd25519Key" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.52.0" + } + ] + }, + { + "id": "GO-2026-5932", + "imports": [ + { + "path": "golang.org/x/crypto/openpgp", + "symbols": [] + }, + { + "path": "golang.org/x/crypto/openpgp/packet", + "symbols": [] + }, + { + "path": "golang.org/x/crypto/openpgp/armor", + "symbols": [] + }, + { + "path": "golang.org/x/crypto/openpgp/clearsign", + "symbols": [] + }, + { + "path": "golang.org/x/crypto/openpgp/errors", + "symbols": [] + }, + { + "path": "golang.org/x/crypto/openpgp/elgamal", + "symbols": [] + }, + { + "path": "golang.org/x/crypto/openpgp/s2k", + "symbols": [] + } + ], + "ranges": [ + { + "introduced": "0" + } + ] + }, + { + "id": "GO-2026-6303", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "NewServerConn", + "connection.serverAuthenticate" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-6354", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn", + "NewControlClientConn", + "NewServerConn", + "channel.Accept", + "channel.handlePacket", + "channel.responseMessageReceived" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.56.0" + } + ] + }, + { + "id": "GO-2026-6355", + "imports": [ + { + "path": "golang.org/x/crypto/ssh", + "symbols": [ + "Dial", + "NewClientConn", + "NewControlClientConn", + "NewServerConn", + "channel.handlePacket" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.56.0" + } + ] + } + ], + "golang.org/x/net": [ + { + "id": "GO-2020-0014", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "inSelectIM", + "inSelectInTableIM" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20190125091013-d26f9f9a57f3" + } + ] + }, + { + "id": "GO-2021-0078", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "inBodyIM", + "inFramesetIM" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20180816102801-aaf60122140d" + } + ] + }, + { + "id": "GO-2021-0238", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "inHeadIM" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20210520170846-37e1c6afe023" + } + ] + }, + { + "id": "GO-2022-0192", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "parser.resetInsertionMode" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20180925071336-cf3bd585ca2a" + } + ] + }, + { + "id": "GO-2022-0193", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "inBodyIM" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20180921000356-2f5d2388922f" + } + ] + }, + { + "id": "GO-2022-0197", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "nodeStack.contains" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20190125002852-4b62a64f59f7" + } + ] + }, + { + "id": "GO-2022-0236", + "imports": [ + { + "path": "golang.org/x/net/http/httpguts", + "symbols": [ + "HeaderValuesContainsToken", + "headerValueContainsToken" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20210428140749-89ef3d95e781" + } + ] + }, + { + "id": "GO-2022-0288", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Server.ServeConn", + "serverConn.canonicalHeader" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20211209124913-491a49abca63" + } + ] + }, + { + "id": "GO-2022-0536", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Server.ServeConn", + "serverConn.scheduleFrameWrite", + "serverConn.serve", + "serverConn.writeFrame" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20190813141303-74dc4d7220e7" + } + ] + }, + { + "id": "GO-2022-0969", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Server.ServeConn", + "serverConn.goAway" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20220906165146-f3363e06e74c" + } + ] + }, + { + "id": "GO-2022-1144", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Server.ServeConn", + "serverConn.canonicalHeader" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.4.0" + } + ] + }, + { + "id": "GO-2023-1495", + "imports": [ + { + "path": "golang.org/x/net/http2/h2c", + "symbols": [ + "h2cHandler.ServeHTTP", + "h2cUpgrade" + ] + } + ], + "ranges": [ + { + "introduced": "0.0.0-20220524220425-1d687d428aca" + }, + { + "fixed": "0.1.1-0.20221104162952-702349b0e862" + } + ] + }, + { + "id": "GO-2023-1571", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.RoundTrip", + "pipe.Read", + "priorityWriteScheduler.CloseStream", + "priorityWriteScheduler.OpenStream", + "pseudoHeaderError.Error", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "serverConn.CloseConn", + "serverConn.Flush", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "writeData.String" + ] + }, + { + "path": "golang.org/x/net/http2/hpack", + "symbols": [ + "Decoder.DecodeFull", + "Decoder.Write", + "Decoder.parseFieldLiteral", + "Decoder.readString" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.7.0" + } + ] + }, + { + "id": "GO-2023-1988", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Render", + "render1" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.13.0" + } + ] + }, + { + "id": "GO-2023-2102", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Server.ServeConn", + "serverConn.processHeaders", + "serverConn.runHandler", + "serverConn.serve", + "serverConn.upgradeRequest" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.17.0" + } + ] + }, + { + "id": "GO-2024-2687", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "Framer.readMetaFrame", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.RoundTrip", + "pipe.Read", + "priorityWriteScheduler.CloseStream", + "priorityWriteScheduler.OpenStream", + "pseudoHeaderError.Error", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "roundRobinWriteScheduler.OpenStream", + "serverConn.CloseConn", + "serverConn.Flush", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "writeData.String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.23.0" + } + ] + }, + { + "id": "GO-2024-3333", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "htmlIntegrationPoint", + "inBodyIM", + "inTableIM", + "parseDoctype" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.33.0" + } + ] + }, + { + "id": "GO-2025-3503", + "imports": [ + { + "path": "golang.org/x/net/http/httpproxy", + "symbols": [ + "config.useProxy", + "domainMatch.match" + ] + }, + { + "path": "golang.org/x/net/proxy", + "symbols": [ + "Dial", + "FromEnvironment", + "FromEnvironmentUsing", + "PerHost.AddFromString", + "PerHost.Dial", + "PerHost.DialContext", + "PerHost.dialerForRequest" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.36.0" + } + ] + }, + { + "id": "GO-2025-3595", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "Tokenizer.Next", + "Tokenizer.readStartTag" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.38.0" + } + ] + }, + { + "id": "GO-2026-4440", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "parser.parse" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.45.0" + } + ] + }, + { + "id": "GO-2026-4441", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "inRowIM" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.45.0" + } + ] + }, + { + "id": "GO-2026-4559", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.ReadFrameForHeader", + "Framer.ReadFrameHeader", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePriorityUpdate", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "bufferedWriterTimeoutWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "netHTTPClientConn.Close", + "netHTTPClientConn.RoundTrip", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "pipe.Read", + "priorityWriteSchedulerRFC7540.CloseStream", + "priorityWriteSchedulerRFC7540.OpenStream", + "priorityWriteSchedulerRFC9218.OpenStream", + "pseudoHeaderError.Error", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "roundRobinWriteScheduler.OpenStream", + "serverConn.CloseConn", + "serverConn.Flush", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "typeFrameParser", + "unencryptedTransport.RoundTrip", + "writeData.String" + ] + } + ], + "ranges": [ + { + "introduced": "0.50.0" + }, + { + "fixed": "0.51.0" + } + ] + }, + { + "id": "GO-2026-4918", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "clientConnPool.GetClientConn", + "clientConnReadLoop.processSettingsNoWrite", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "unencryptedTransport.RoundTrip" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.53.0" + } + ] + }, + { + "id": "GO-2026-5025", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "parser.parse" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-5026", + "imports": [ + { + "path": "golang.org/x/net/idna", + "symbols": [ + "Profile.ToASCII", + "Profile.ToUnicode", + "Profile.process", + "ToASCII", + "ToUnicode" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-5027", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "parser.parse" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-5028", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "parser.parse" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-5029", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "parser.parse" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-5030", + "imports": [ + { + "path": "golang.org/x/net/html", + "symbols": [ + "Parse", + "ParseFragment", + "ParseFragmentWithOptions", + "ParseWithOptions", + "parser.parse" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.55.0" + } + ] + }, + { + "id": "GO-2026-5942", + "imports": [ + { + "path": "golang.org/x/net/dns/dnsmessage", + "symbols": [ + "Message.Unpack", + "Parser.Additional", + "Parser.AllAdditionals", + "Parser.AllAnswers", + "Parser.AllAuthorities", + "Parser.Answer", + "Parser.Authority", + "Parser.HTTPSResource", + "Parser.SVCBResource", + "unpackSVCBResource" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.56.0" + } + ] + }, + { + "id": "GO-2026-6603", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.ReadFrameForHeader", + "Framer.ReadFrameHeader", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePriorityUpdate", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "Framer.readMetaFrame", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "bufferedWriterTimeoutWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "netHTTPClientConn.Close", + "netHTTPClientConn.RoundTrip", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "pipe.Read", + "priorityWriteSchedulerRFC7540.CloseStream", + "priorityWriteSchedulerRFC7540.OpenStream", + "priorityWriteSchedulerRFC9218.OpenStream", + "pseudoHeaderError.Error", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "roundRobinWriteScheduler.OpenStream", + "serverConn.CloseConn", + "serverConn.Flush", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "unencryptedTransport.RoundTrip", + "writeData.String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.60.0" + } + ] + }, + { + "id": "GO-2026-6610", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "clientConnPool.GetClientConn", + "clientConnReadLoop.handleResponse", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "unencryptedTransport.RoundTrip" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.60.0" + } + ] + }, + { + "id": "GO-2026-6611", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ClientConn.addStreamLocked", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.Consume", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.ReadFrameForHeader", + "Framer.ReadFrameHeader", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePriorityUpdate", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Server.serveConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "Transport.newClientConn", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "bufferedWriterTimeoutWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "clientConnReadLoop.processSettingsNoWrite", + "clientConnReadLoop.processWindowUpdate", + "clientConnReadLoop.streamByID", + "clientStream.awaitFlowControl", + "clientStream.cleanupWriteRequest", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "netHTTPClientConn.Close", + "netHTTPClientConn.RoundTrip", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "outflow.add", + "outflow.available", + "outflow.setConnFlow", + "outflow.take", + "pipe.Read", + "priorityWriteSchedulerRFC7540.CloseStream", + "priorityWriteSchedulerRFC7540.OpenStream", + "priorityWriteSchedulerRFC7540.Pop", + "priorityWriteSchedulerRFC9218.OpenStream", + "priorityWriteSchedulerRFC9218.Pop", + "pseudoHeaderError.Error", + "randomWriteScheduler.Pop", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "roundRobinWriteScheduler.OpenStream", + "roundRobinWriteScheduler.Pop", + "serverConn.CloseConn", + "serverConn.Flush", + "serverConn.newStream", + "serverConn.processSettingInitialWindowSize", + "serverConn.processWindowUpdate", + "serverConn.scheduleFrameWrite", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "unencryptedTransport.RoundTrip", + "writeData.String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.60.0" + } + ] + }, + { + "id": "GO-2026-6612", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.ReadFrameForHeader", + "Framer.ReadFrameHeader", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePriorityUpdate", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "bufferedWriterTimeoutWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "netHTTPClientConn.Close", + "netHTTPClientConn.RoundTrip", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "pipe.Read", + "priorityWriteSchedulerRFC7540.CloseStream", + "priorityWriteSchedulerRFC7540.OpenStream", + "priorityWriteSchedulerRFC9218.OpenStream", + "pseudoHeaderError.Error", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "roundRobinWriteScheduler.OpenStream", + "serverConn.CloseConn", + "serverConn.Flush", + "serverConn.closeStream", + "serverConn.handlerDone", + "serverConn.newWriterAndRequest", + "serverConn.newWriterAndRequestNoBody", + "serverConn.noteBodyRead", + "serverConn.processHeaders", + "serverConn.runHandler", + "serverConn.scheduleHandler", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "unencryptedTransport.RoundTrip", + "writeData.String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.60.0" + } + ] + }, + { + "id": "GO-2026-6617", + "imports": [ + { + "path": "golang.org/x/net/http2", + "symbols": [ + "ClientConn.Close", + "ClientConn.Ping", + "ClientConn.RoundTrip", + "ClientConn.Shutdown", + "ConfigureServer", + "ConfigureTransport", + "ConfigureTransports", + "ConnectionError.Error", + "ErrCode.String", + "FrameHeader.String", + "FrameType.String", + "FrameWriteRequest.String", + "Framer.ReadFrame", + "Framer.ReadFrameForHeader", + "Framer.ReadFrameHeader", + "Framer.WriteContinuation", + "Framer.WriteData", + "Framer.WriteDataPadded", + "Framer.WriteGoAway", + "Framer.WriteHeaders", + "Framer.WritePing", + "Framer.WritePriority", + "Framer.WritePriorityUpdate", + "Framer.WritePushPromise", + "Framer.WriteRSTStream", + "Framer.WriteRawFrame", + "Framer.WriteSettings", + "Framer.WriteSettingsAck", + "Framer.WriteWindowUpdate", + "GoAwayError.Error", + "ReadFrameHeader", + "Server.ServeConn", + "Setting.String", + "SettingID.String", + "SettingsFrame.ForeachSetting", + "StreamError.Error", + "Transport.CloseIdleConnections", + "Transport.NewClientConn", + "Transport.RoundTrip", + "Transport.RoundTripOpt", + "bufferedWriter.Flush", + "bufferedWriter.Write", + "bufferedWriterTimeoutWriter.Write", + "chunkWriter.Write", + "clientConnPool.GetClientConn", + "connError.Error", + "dataBuffer.Read", + "duplicatePseudoHeaderError.Error", + "gzipReader.Close", + "gzipReader.Read", + "headerFieldNameError.Error", + "headerFieldValueError.Error", + "netHTTPClientConn.Close", + "netHTTPClientConn.RoundTrip", + "noDialClientConnPool.GetClientConn", + "noDialH2RoundTripper.NewClientConn", + "noDialH2RoundTripper.RoundTrip", + "pipe.Read", + "priorityWriteSchedulerRFC7540.CloseStream", + "priorityWriteSchedulerRFC7540.OpenStream", + "priorityWriteSchedulerRFC9218.OpenStream", + "pseudoHeaderError.Error", + "requestBody.Close", + "requestBody.Read", + "responseWriter.Flush", + "responseWriter.FlushError", + "responseWriter.Push", + "responseWriter.SetReadDeadline", + "responseWriter.SetWriteDeadline", + "responseWriter.Write", + "responseWriter.WriteHeader", + "responseWriter.WriteString", + "roundRobinWriteScheduler.OpenStream", + "serverConn.CloseConn", + "serverConn.Flush", + "serverConn.processSetting", + "serverConn.startFrameWrite", + "stickyErrWriter.Write", + "transportResponseBody.Close", + "transportResponseBody.Read", + "unencryptedTransport.RoundTrip", + "writeData.String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.60.0" + } + ] + } + ], + "golang.org/x/oauth2": [ + { + "id": "GO-2025-3488", + "imports": [ + { + "path": "golang.org/x/oauth2/jws", + "symbols": [ + "Verify" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.27.0" + } + ] + } + ], + "golang.org/x/sys": [ + { + "id": "GO-2022-0493", + "imports": [ + { + "path": "golang.org/x/sys/unix", + "symbols": [ + "Faccessat" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.0.0-20220412211240-33da011f77ad" + } + ] + }, + { + "id": "GO-2026-5024", + "imports": [ + { + "path": "golang.org/x/sys/windows", + "symbols": [ + "NewNTUnicodeString" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.44.0" + } + ] + } + ], + "golang.org/x/text": [ + { + "id": "GO-2020-0015", + "imports": [ + { + "path": "golang.org/x/text/encoding/unicode", + "symbols": [ + "bomOverride.Transform", + "utf16Decoder.Transform" + ] + }, + { + "path": "golang.org/x/text/transform", + "symbols": [ + "String" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.3.3" + } + ] + }, + { + "id": "GO-2021-0113", + "imports": [ + { + "path": "golang.org/x/text/language", + "symbols": [ + "MatchStrings", + "MustParse", + "Parse", + "ParseAcceptLanguage" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.3.7" + } + ] + }, + { + "id": "GO-2022-1059", + "imports": [ + { + "path": "golang.org/x/text/language", + "symbols": [ + "MatchStrings", + "ParseAcceptLanguage" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.3.8" + } + ] + }, + { + "id": "GO-2026-5970", + "imports": [ + { + "path": "golang.org/x/text/unicode/norm", + "symbols": [ + "Form.Append", + "Form.AppendString", + "Form.Bytes", + "Form.FirstBoundary", + "Form.FirstBoundaryInString", + "Form.IsNormal", + "Form.IsNormalString", + "Form.LastBoundary", + "Form.NextBoundary", + "Form.NextBoundaryInString", + "Form.Properties", + "Form.PropertiesString", + "Form.QuickSpan", + "Form.QuickSpanString", + "Form.Span", + "Form.SpanString", + "Form.String", + "Form.Transform", + "Iter.Init", + "Iter.InitString", + "Iter.Next", + "Iter.Seek", + "nextComposed", + "normReader.Read", + "normWriter.Write" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.39.0" + } + ] + }, + { + "id": "GO-2026-6629", + "imports": [ + { + "path": "golang.org/x/text/secure/precis", + "symbols": [ + "Profile.Append", + "Profile.AppendCompareKey", + "Profile.Bytes", + "Profile.Compare", + "Profile.CompareKey", + "Profile.NewTransformer", + "Profile.String", + "Transformer.Bytes", + "Transformer.String", + "Transformer.Transform", + "nickAdditionalMapping.Transform" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "0.41.0" + } + ] + } + ], + "google.golang.org/grpc": [ + { + "id": "GO-2023-2153", + "imports": [ + { + "path": "google.golang.org/grpc/internal/transport", + "symbols": [ + "NewServerTransport" + ] + }, + { + "path": "google.golang.org/grpc", + "symbols": [ + "NewServer", + "Server.Serve", + "Server.initServerWorkers" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.56.3" + }, + { + "introduced": "1.57.0" + }, + { + "fixed": "1.57.1" + }, + { + "introduced": "1.58.0" + }, + { + "fixed": "1.58.3" + } + ] + }, + { + "id": "GO-2024-2978", + "imports": [ + { + "path": "google.golang.org/grpc/metadata", + "symbols": [ + "MD.String" + ] + } + ], + "ranges": [ + { + "introduced": "1.64.0" + }, + { + "fixed": "1.64.1" + } + ] + }, + { + "id": "GO-2026-4762", + "imports": [ + { + "path": "google.golang.org/grpc", + "symbols": [ + "Server.Serve", + "Server.ServeHTTP", + "Server.handleStream" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.79.3" + } + ] + }, + { + "id": "GO-2026-6061", + "imports": [ + { + "path": "google.golang.org/grpc/internal/transport", + "symbols": [ + "ClientStream.Close", + "ClientStream.Header", + "ClientStream.Read", + "ClientStream.RecvCompress", + "ClientStream.TrailersOnly", + "ClientStream.Write", + "NewHTTP2Client", + "NewServerTransport", + "ServerStream.Read", + "ServerStream.SendHeader", + "ServerStream.Write", + "ServerStream.WriteStatus", + "Stream.ReadMessageHeader", + "controlBuffer.executeAndPut", + "controlBuffer.getOnceLocked", + "http2Client.Close", + "http2Client.GracefulClose", + "http2Client.NewStream", + "http2Server.Drain", + "http2Server.HandleStreams", + "recvBufferReader.Read", + "recvBufferReader.ReadMessageHeader", + "transportReader.Read", + "transportReader.ReadMessageHeader" + ] + }, + { + "path": "google.golang.org/grpc/internal/xds/rbac", + "symbols": [ + "ChainEngine.IsAuthorized", + "NewChainEngine", + "matchersFromPermissions", + "matchersFromPrincipals" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.82.1" + } + ] + }, + { + "id": "GO-2026-6348", + "imports": [ + { + "path": "google.golang.org/grpc/internal/transport", + "symbols": [ + "ClientStream.Close", + "ClientStream.Header", + "ClientStream.Read", + "ClientStream.RecvCompress", + "ClientStream.TrailersOnly", + "NewHTTP2Client", + "NewServerTransport", + "ServerStream.Read", + "Stream.ReadMessageHeader", + "http2Client.Close", + "http2Client.GracefulClose", + "http2Client.NewStream", + "http2Client.newStream", + "http2Server.Close", + "http2Server.HandleStreams", + "http2Server.operateHeaders", + "recvBuffer.init", + "recvBuffer.load", + "recvBuffer.put", + "recvBufferReader.Read", + "recvBufferReader.ReadMessageHeader", + "serverHandlerTransport.HandleStreams", + "transportReader.Read", + "transportReader.ReadMessageHeader" + ] + }, + { + "path": "google.golang.org/grpc/mem", + "symbols": [ + "BufferSlice.MaterializeToBuffer", + "Copy", + "IsBelowBufferPoolingThreshold", + "NewBuffer", + "ReadAll", + "writer.Write" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.83.1" + } + ] + }, + { + "id": "GO-2026-6441", + "imports": [ + { + "path": "google.golang.org/grpc/internal/xds/httpfilter/rbac", + "symbols": [ + "builder.ParseFilterConfig", + "builder.ParseFilterConfigOverride", + "parseConfig" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.83.1" + } + ] + }, + { + "id": "GO-2026-6443", + "imports": [ + { + "path": "google.golang.org/grpc/internal/transport", + "symbols": [ + "http2Server.HandleStreams", + "http2Server.operateHeaders" + ] + }, + { + "path": "google.golang.org/grpc/internal/xds/server", + "symbols": [ + "RouteAndProcess" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.82.2" + }, + { + "introduced": "1.83.0" + }, + { + "fixed": "1.83.2" + }, + { + "introduced": "1.84.0-dev" + }, + { + "fixed": "1.84.0-dev.0.20260825144003-d5a41119e0e3" + }, + { + "introduced": "1.85.0-dev" + }, + { + "fixed": "1.85.0-dev.0.20260825072537-93e31b48545e" + } + ] + } + ], + "google.golang.org/protobuf": [ + { + "id": "GO-2023-1631", + "imports": [ + { + "path": "google.golang.org/protobuf/encoding/prototext", + "symbols": [ + "Unmarshal", + "UnmarshalOptions.Unmarshal", + "UnmarshalOptions.unmarshal" + ] + }, + { + "path": "google.golang.org/protobuf/internal/encoding/text", + "symbols": [ + "Decoder.Peek", + "Decoder.Read", + "parseNumber" + ] + } + ], + "ranges": [ + { + "introduced": "1.29.0" + }, + { + "fixed": "1.29.1" + } + ] + }, + { + "id": "GO-2024-2611", + "imports": [ + { + "path": "google.golang.org/protobuf/encoding/protojson", + "symbols": [ + "Unmarshal", + "UnmarshalOptions.Unmarshal", + "UnmarshalOptions.unmarshal" + ] + }, + { + "path": "google.golang.org/protobuf/internal/encoding/json", + "symbols": [ + "Decoder.Peek", + "Decoder.Read" + ] + } + ], + "ranges": [ + { + "introduced": "0" + }, + { + "fixed": "1.33.0" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/projectgraph/public/methods/corpus.txt b/test/projectgraph/public/methods/corpus.txt new file mode 100644 index 00000000..06069a2e --- /dev/null +++ b/test/projectgraph/public/methods/corpus.txt @@ -0,0 +1,23 @@ +# The projects of the method measure, one per line: the language, a name, the +# project as module@version or crate@version, and each dependency that an +# advisory of advisories.json names at its pinned version. +go bluemonday github.com/microcosm-cc/bluemonday@v1.0.16 golang.org/x/net@v0.0.0-20210614182718-04defd469f4e +go goquery github.com/PuerkitoBio/goquery@v1.8.0 golang.org/x/net@v0.0.0-20210916014120-12bc252f5db8 +go colly github.com/gocolly/colly/v2@v2.1.0 github.com/antchfx/xmlquery@v1.2.4 github.com/antchfx/xpath@v1.1.8 golang.org/x/crypto@v0.0.0-20190605123033-f99c8df09eb5 golang.org/x/net@v0.0.0-20200602114024-627f9648deb9 google.golang.org/protobuf@v1.24.0 +go hey github.com/rakyll/hey@v0.1.4 golang.org/x/net@v0.0.0-20181017193950-04a2e542c03f golang.org/x/text@v0.3.0 +go melody github.com/olahol/melody@v1.1.1 github.com/gorilla/websocket@v1.5.0 +go scany github.com/georgysavva/scany/v2@v2.0.0 github.com/jackc/pgx/v5@v5.0.0 golang.org/x/crypto@v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/sys@v0.0.0-20210823070655-63515b42dcdf golang.org/x/text@v0.3.7 +go fiber github.com/gofiber/fiber/v2@v2.40.0 github.com/valyala/fasthttp@v1.41.0 golang.org/x/sys@v0.0.0-20220811171246-fbc7d0a398ab +go sftp github.com/pkg/sftp@v1.13.5 golang.org/x/crypto@v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/sys@v0.0.0-20211216021012-1d35b9e2eb4e +go grpc-middleware github.com/grpc-ecosystem/go-grpc-middleware@v1.3.0 github.com/sirupsen/logrus@v1.4.2 golang.org/x/net@v0.0.0-20201021035429-f5854403a974 golang.org/x/oauth2@v0.0.0-20180821212333-d2e6202438be google.golang.org/grpc@v1.29.1 +go go-rabbitmq github.com/wagslane/go-rabbitmq@v0.12.1 github.com/rabbitmq/amqp091-go@v1.5.0 +rust postgres postgres@0.19.7 tokio@1.13.0 tokio-postgres@0.7.17 +rust parking_lot parking_lot@0.11.0 lock_api@0.4.1 +rust native-tls native-tls@0.2.10 openssl@0.10.71 +rust cookie cookie@0.14.4 time@0.2.22 rand@0.8.5 +rust imageproc imageproc@0.21.0 image@0.23.11 rand@0.7.3 rulinalg@0.4.2 +rust cssparser cssparser@0.27.2 smallvec@1.6.0 +rust selectors selectors@0.22.0 smallvec@1.6.0 +rust tokio-postgres tokio-postgres@0.7.7 postgres-protocol@0.6.11 tokio@1.13.0 +rust lettre_email lettre_email@0.9.4 lettre@0.9.5 time@0.1.45 +rust rusqlite_migration rusqlite_migration@0.5.0 rusqlite@0.22.0 diff --git a/test/projectgraph/receivers_test.go b/test/projectgraph/receivers_test.go new file mode 100644 index 00000000..051fa023 --- /dev/null +++ b/test/projectgraph/receivers_test.go @@ -0,0 +1,347 @@ +package projectgraph + +import ( + "context" + iofs "io/fs" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/safedep/code/core" + "github.com/safedep/code/fileindex" + "github.com/safedep/code/fileindex/store/memstore" + codefs "github.com/safedep/code/fs" + "github.com/safedep/code/plugin/indexer" + "github.com/safedep/code/projectgraph" + "github.com/safedep/code/projectgraph/reach" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// receiverRow is one form of the receiver of a method call of a type of a +// dependency. want is the answer for the target, as found or +// uncertain/. A row that the link step does not answer yet names the +// issue that fixes it in gap, and its answer before that fix in today. +type receiverRow struct { + name string + source string + target string + want string + gap string + today string + // noDeps leaves the dependency code out of the graph. + noDeps bool +} + +// receiverUnits are the units of dependency code of the receiver rows, by +// language: a directory under testdata/receivers and its unit. +var receiverUnits = map[core.LanguageCode][2]string{ + core.LanguageCodeGo: {"go/dep", "go:example.com/dep@v1.0.0"}, + core.LanguageCodeRust: {"rust/dep", "cargo:dep@1.0.0"}, +} + +const ( + goReceiverManifest = "module app\n\ngo 1.22\n\nrequire example.com/dep v1.0.0\n" + rustReceiverManifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ndep = \"1.0.0\"\n" +) + +var goReceiverRows = []receiverRow{ + { + name: "declared variable", + source: "func run() {\n\tvar t *dep.Thing = dep.NewThing()\n\tt.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "parameter", + source: "func run(t *dep.Thing) {\n\tt.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "struct field", + source: "type app struct {\n\tt *dep.Thing\n}\n\nfunc run(a *app) {\n\ta.t.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "first-party function return", + source: "func make() *dep.Thing { return nil }\n\nfunc run() {\n\tt := make()\n\tt.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "external function return", + source: "func run() {\n\tt := dep.NewThing()\n\tt.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "multi-return", + source: "func run() {\n\tt, err := dep.NewPair()\n\tif err != nil {\n\t\treturn\n\t}\n\tt.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "chained call", + source: "func run() {\n\tdep.NewThing().Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "chained method call", + source: "func run() {\n\tdep.NewThing().Next().Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "interface return", + source: "func run() {\n\td := dep.NewDoer()\n\td.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "found", + }, + { + name: "interface return with no implementation", + source: "func run() {\n\tr := dep.NewRunner()\n\tr.Run()\n}", + target: "example.com//other//Job//Run", + want: "uncertain/interface-receiver", + }, + { + name: "external function return with no dependency code", + source: "func run() {\n\tt := dep.NewThing()\n\tt.Do()\n}", + target: "example.com//dep//Thing//Do", + want: "uncertain/no-dependency-source", + noDeps: true, + }, +} + +var rustReceiverRows = []receiverRow{ + { + name: "declared variable", + source: "fn run() {\n let mut t: Thing = Thing::new();\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "parameter", + source: "fn run(mut t: Thing) {\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "reference and generic parameter", + source: "fn run(t: &mut Thing<[u8; 4]>) {\n t.insert([1; 4]);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "struct field", + source: "struct App {\n t: Thing,\n}\n\nfn run(a: &mut App) {\n a.t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "first-party function return", + source: "fn make() -> Thing {\n Thing::new()\n}\n\nfn run() {\n let mut t = make();\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "external function return", + source: "fn run() {\n let mut t = dep::new_thing();\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "multi-return with ?", + source: "fn run() -> Result<(), String> {\n let mut t = dep::load_thing()?;\n t.insert(1);\n Ok(())\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "option unwrap", + source: "fn run() {\n let mut t = dep::try_thing().unwrap();\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "chained call", + source: "fn run() {\n dep::new_thing().insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "associated function", + source: "fn run() {\n let mut t = Thing::from_vec(vec![1u8]);\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "associated function with type arguments", + source: "fn run() {\n let mut t = Thing::::new();\n t.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "clone of a typed value", + source: "fn run() {\n let a: Thing = Thing::new();\n let mut b = a.clone();\n b.insert(1);\n}", + target: "dep//Thing//insert", + want: "found", + }, + { + name: "trait object return", + source: "fn run() {\n let d = dep::doer();\n d.run();\n}", + target: "dep//Thing//run", + want: "found", + }, + { + name: "generic bound", + source: "fn run(d: &T) {\n d.run();\n}", + target: "dep//Thing//run", + want: "found", + }, + { + name: "trait object with no implementation", + source: "fn run(r: &dyn Runner) {\n r.start();\n}", + target: "dep//Job//start", + want: "uncertain/trait-receiver", + }, +} + +func TestReceiverForms(t *testing.T) { + languages := caseLanguages(t) + for _, tc := range []struct { + language core.LanguageCode + rows []receiverRow + files func(source string) map[string]string + }{ + {core.LanguageCodeGo, goReceiverRows, goReceiverProject}, + {core.LanguageCodeRust, rustReceiverRows, rustReceiverProject}, + } { + for _, row := range tc.rows { + t.Run(string(tc.language)+"/"+row.name, func(t *testing.T) { + got := receiverAnswer(t, languages, tc.language, tc.files(row.source), row) + if row.gap != "" { + t.Logf("known gap, %s fixes it: the answer is %s, want %s", row.gap, got, row.want) + assert.Equal(t, row.today, got, "the answer before %s changed: update the row", row.gap) + return + } + assert.Equal(t, row.want, got) + }) + } + } +} + +// goReceiverProject is a Go project whose main.go holds the source. +func goReceiverProject(source string) map[string]string { + return map[string]string{ + "go.mod": goReceiverManifest, + "main.go": "package main\n\nimport \"example.com/dep\"\n\n" + source + "\n\nfunc main() {}\n", + } +} + +// rustReceiverProject is a Rust crate whose src/main.rs holds the source +// and the uses of the dependency. +func rustReceiverProject(source string) map[string]string { + return map[string]string{ + "Cargo.toml": rustReceiverManifest, + "src/main.rs": "use dep::{Doer, Runner, Thing};\n\n" + source + "\n\nfn main() {}\n", + } +} + +// receiverAnswer links a project of the files with the dependency code of +// the language and answers the target of the row. +func receiverAnswer(t *testing.T, languages []core.Language, language core.LanguageCode, files map[string]string, row receiverRow) string { + dir := t.TempDir() + for p, text := range files { + require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, p)), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, p), []byte(text), 0o644)) + } + cfg := codefs.LocalFileSystemConfig{AppDirectories: []string{dir}, ImportUnits: map[string]string{}} + units := map[string]iofs.FS{} + if !row.noDeps { + u := receiverUnits[language] + depDir, err := filepath.Abs(filepath.Join("testdata", "receivers", filepath.FromSlash(u[0]))) + require.NoError(t, err) + cfg.ImportDirectories = []string{depDir} + cfg.ImportUnits[depDir] = u[1] + units[u[1]] = os.DirFS(depDir) + } + fsys, err := codefs.NewLocalFileSystem(cfg) + require.NoError(t, err) + indexed := indexProject(t, fsys, indexer.Options{Languages: languages, Store: memstore.New(), Imports: len(units) > 0}) + g := linkProject(t, dir, indexed, projectgraph.Options{Units: units}) + source := receiverSource(t, language) + results, err := reach.Reach(context.Background(), g, reach.Query{Targets: targetsOf(t, row.target), Sources: []reach.Source{source}, SourceMode: reach.SourcesOnly}) + require.NoError(t, err) + return outcome(results[0], receiverReasons(t, g, []string{source.Key()})) +} + +// receiverSource is the run function of the main file of the language, +// which the search starts from. +func receiverSource(t *testing.T, language core.LanguageCode) reach.Source { + p := "main.go" + if language == core.LanguageCodeRust { + p = "src/main.rs" + } + s, err := reach.SourceByKey(fileindex.Join(p, "run")) + require.NoError(t, err) + return s +} + +// outcome is the answer for one target: found, not-found, no-such-target, +// or uncertain with its cause. +func outcome(r reach.Result, methods map[string]string) string { + if r.Status != reach.Uncertain { + return string(r.Status) + } + return "uncertain/" + uncertainCause(r, methods) +} + +var reasonText = regexp.MustCompile(`\(([a-z-]+)\)$`) + +// reasonOf names an uncertainty by its kind, and an unresolved call by its +// reason. +func reasonOf(u reach.Uncertainty) string { + if m := reasonText.FindStringSubmatch(u.Text); u.Kind == reach.UnresolvedCall && m != nil { + return m[1] + } + return string(u.Kind) +} + +// receiverReasons returns the reason of each call site that a search from +// the keys reaches, by the method name of the site. An unknown receiver +// wins over another reason of the name. A result lists at most three +// uncertainties of a kind, so the reasons come from the whole list of an +// exploration. +func receiverReasons(t testing.TB, g *projectgraph.Graph, from []string) map[string]string { + e, err := reach.Explore(context.Background(), g, from, 0) + require.NoError(t, err) + out := map[string]string{} + for _, u := range e.Uncertainties { + if u.Kind != reach.UnresolvedCall || u.Method == "" { + continue + } + if reason := reasonOf(u); out[u.Method] == "" || reason == string(fileindex.UnresolvedUnknownReceiver) { + out[u.Method] = reason + } + } + return out +} + +// uncertainCause names the cause of an uncertain answer. A reached call of +// the method of the target whose receiver the link step does not know +// comes first. Else the cause is the first uncertainty of the result that +// the root fallback does not explain. +func uncertainCause(r reach.Result, methods map[string]string) string { + if reason, ok := methods[fileindex.Last(r.Target.Key())]; ok { + return reason + } + for _, u := range r.Uncertainties { + if u.Kind != reach.RootFallback { + return reasonOf(u) + } + } + return string(reach.RootFallback) +} diff --git a/test/projectgraph/report.md b/test/projectgraph/report.md index a4f2cce0..d2c602a6 100644 --- a/test/projectgraph/report.md +++ b/test/projectgraph/report.md @@ -854,3 +854,186 @@ cases and 82.2% for Jelly on swarm-js. This report did not run PyCG or Jelly, and its exact-cases metric counts whole cases, so the numbers do not compare one to one. The recall gate of the design needs a run of PyCG and Jelly on the same cases. + +## Method targets + +This section measures how often a target of a vulnerable function or +method of a dependency ends uncertain on real projects. `README.md` says +how to run it. The "before" run is from the commit that adds this +section, on 2026-10-19, on the machine above. + +### The corpus + +`public/methods/corpus.txt` lists 10 Go projects and 10 Rust projects. +Each one depends on a module or a crate with function or method symbols +in an advisory. `public/methods/advisories.json` holds the advisories of +these dependencies: the Go vulndb (`vuln.go.dev`) and RustSec through +OSV, as of 2026-10-19. + +| Language | Projects | Dependencies with an advisory | +| --- | --- | --- | +| Go | bluemonday v1.0.16, goquery v1.8.0, colly v2.1.0, hey v0.1.4, melody v1.1.1, scany v2.0.0, fiber v2.40.0, sftp v1.13.5, go-grpc-middleware v1.3.0, go-rabbitmq v0.12.1 | golang.org/x/net, golang.org/x/crypto, golang.org/x/text, golang.org/x/sys, golang.org/x/oauth2, github.com/antchfx/xpath, github.com/antchfx/xmlquery, google.golang.org/protobuf, google.golang.org/grpc, github.com/gorilla/websocket, github.com/jackc/pgx/v5, github.com/valyala/fasthttp, github.com/sirupsen/logrus, github.com/rabbitmq/amqp091-go | +| Rust | postgres 0.19.7, parking_lot 0.11.0, native-tls 0.2.10, cookie 0.14.4, imageproc 0.21.0, cssparser 0.27.2, selectors 0.22.0, tokio-postgres 0.7.7, lettre_email 0.9.4, rusqlite_migration 0.5.0 | tokio, tokio-postgres, postgres-protocol, lock_api, openssl, time, rand, image, rulinalg, smallvec, lettre, rusqlite | + +A Go dependency has the version that the go.mod of the project names. A +Rust dependency has the newest version that the requirement of the +Cargo.toml admits and that an advisory affects, as a lock file at a +vulnerable version would pin it. The code of each such dependency is a +unit of the link step. Every symbol of an advisory that affects the +version of a dependency is a target. A Go advisory with no symbol list +covers its whole package and gives no target. A Rust name with type +arguments is no key and gives no target. A Rust name whose last segment +starts with an upper case letter is a type, and the table counts it as +`other`. + +The test runs no network call and the corpus is not in the repository, +so a run needs `scripts/fetch_method_corpus.sh` first. The real Go +standard library is not in the corpus, so the measure has no target of +the standard library. + +### What the columns mean + +A target ends as `found`, `not-found`, `no-such-target` or `uncertain`. +An uncertain target has one cause. When the search from the roots +reaches an unresolved call whose method name is the last segment of the +target, the cause is the reason of that call, and `unknown-receiver` +wins over another reason. Else the cause is the first uncertainty of the +result, as `budget`, `ambiguous` or `computed-name`. A library project +has no main, so every exported symbol is a root. + +### Results + +The "before" rows come from the commit of FOU-542, before any fix of this +measure. The "FOU-543" rows come from the commit of FOU-543, which also +holds the fixes of FOU-545, FOU-546 and FOU-547 before it. The "FOU-544" +rows come from the commit of FOU-544. The Rust rows of FOU-543 and the Go +function row of FOU-544 do not change, so the table leaves them out. + +| Language | Kind | Run | Targets | found | not-found | no-such-target | unknown-receiver | no-definition | interface-receiver | no-dependency-source | budget | ambiguous | computed-name | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Go | function | before | 256 | 13 | 0 | 7 | 3 | 1 | 0 | 0 | 145 | 53 | 34 | +| Go | function | FOU-543 | 256 | 13 | 0 | 7 | 3 | 0 | 0 | 3 | 39 | 132 | 59 | +| Go | method | before | 1298 | 29 | 2 | 15 | 294 | 119 | 0 | 0 | 576 | 158 | 105 | +| Go | method | FOU-543 | 1298 | 30 | 2 | 15 | 319 | 112 | 3 | 2 | 123 | 459 | 233 | +| Go | method | FOU-544 | 1298 | 30 | 2 | 15 | 320 | 112 | 3 | 2 | 123 | 459 | 232 | +| Rust | function | before | 13 | 0 | 0 | 10 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | +| Rust | function | FOU-544 | 13 | 1 | 0 | 5 | 0 | 5 | 0 | 0 | 0 | 2 | 0 | +| Rust | method | before | 38 | 0 | 1 | 19 | 13 | 5 | 0 | 0 | 0 | 0 | 0 | +| Rust | method | FOU-544 | 38 | 0 | 0 | 7 | 11 | 11 | 0 | 0 | 0 | 9 | 0 | +| Rust | other | before | 5 | 0 | 0 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| Rust | other | FOU-544 | 5 | 0 | 0 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +| Language | Run | Method targets that end as unknown-receiver | +| --- | --- | --- | +| Go | before | 0.227 (294/1298) | +| Go | FOU-543 | 0.246 (319/1298) | +| Go | FOU-544 | 0.247 (320/1298) | +| Rust | before | 0.342 (13/38) | +| Rust | FOU-544 | 0.289 (11/38) | + +Before: + +- Go: `budget` is the largest cause. The index of each + `idna/tables*.go` of x/net hits a limit, so the file is truncated. Each + `gen.go` of x/net has `//go:build ignore` and `package main`, so the + link step records a truncation for it. A `_test.go` file of x/crypto is + truncated too. The budget of a file of a unit concerns every target + once the search enters the unit. hey has 47 truncations and scany 55. +- Go: an `ambiguous` or a `computed-name` call concerns every target. + These calls come from the code of a dependency unit, as pgx and + amqp091-go, and from the project, as fiber and go-grpc-middleware. +- Rust: the link step does not link the code of a `cargo:` unit, so the + graph holds no edge into the crate. A target that no project file names + is `no-such-target`. + +After FOU-543: + +- Go: no project has a truncation of the link step. The graphs of the 10 + Go projects hold 5,286 files, 68% of the 7,810 of before, because the + index step leaves out the test files and the files that no Go build + reads by default. It keeps the files of every platform. `budget` + falls from 576 to 123 method targets. The rest is scany: two + `tables.go` files of x/text are over the size limit of the index step, + so they fail. +- Go: with less budget, the first uncertainty of a result is more often + an `ambiguous` or a `computed-name` call of the code of a unit, which + concerns every target. So these columns grow, and the share of + `unknown-receiver` grows a little. The search reaches more code of the + units through the new edges, and more call sites with an unknown + receiver. +- Go: `found` moves from 29 to 30 method targets. Most symbols of an + advisory are methods that the code of the dependency calls inside its + own module, as the methods of the HTTP/2 server of x/net. A call inside + a unit goes to the definition of the method and not to its external key, + so the search reaches the definition and never meets the target, which + is the external key. The target reads as uncertain when an unresolved + call concerns it, and as not-found when a call of the project that no + root reaches names it. FOU-555 fixes the match. The receiver forms of FOU-543 make the + calls of the project find the method, as the receiver table and the + cases show. They do not change how a target matches code inside a unit. +- The measure has no target of the standard library. The Go vulndb has + the most method symbols for it, and FOU-543 adds `GOROOT/src` as a unit, + but the corpus pins no Go release. + +After FOU-544: + +- Rust: the link step links the code of each crate. The 10 Rust graphs + hold 1,389 files, against 193 of before, and no project has a problem + of a crate that the link step does not link. +- Rust: `no-such-target` falls from 19 to 7 method targets and from 10 to + 5 function targets, because the graph now holds the keys of the crates. + lettre_email finds one function target. The other targets end + uncertain, with an unknown receiver, a call of a name with no + definition, or an `ambiguous` call that the search reaches. As for Go, + a target that only the code of its own crate calls reads as uncertain, + because a call inside a unit goes to the definition and not to the + external key. +- Rust: the five `other` targets of parking_lot name types of lock_api, + as `lock_api::MappedMutexGuard`. A type has no call, so they stay + `no-such-target`. +- Go: the rows move by one target between two causes, because a result + type of an external walk is now always its external key. + +After the review fixes of FOU-544 and FOU-547, the measure gives the same +counts as the FOU-544 rows. The index now keeps the files of every +platform and the bodies of generated files, which changes the graphs and +not the first uncertainty of any result. + +The `unknown-receiver` share counts a target when any reached site has an +unresolved receiver with the method name of the target, also a site +inside a unit. More reached code of the units gives more name matches on +`Next`, `Read` or `ServeHTTP`. So the share rises as the search reaches +more code, and it is not a measure of the receiver forms. The receiver +table of FOU-542 measures those, and every row of it is found. + +After FOU-555: + +| Language | Kind | Targets | found | not-found | no-such-target | unknown-receiver | no-definition | interface-receiver | no-dependency-source | budget | ambiguous | computed-name | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Go | function | 256 | 51 | 0 | 0 | 3 | 5 | 0 | 2 | 39 | 106 | 50 | +| Go | method | 1298 | 331 | 0 | 0 | 154 | 71 | 2 | 2 | 109 | 416 | 213 | +| Rust | function | 13 | 4 | 0 | 0 | 0 | 9 | 0 | 0 | 0 | 0 | 0 | +| Rust | method | 38 | 16 | 0 | 0 | 0 | 13 | 0 | 0 | 9 | 0 | 0 | +| Rust | other | 5 | 0 | 0 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +- A target now matches each definition of a unit whose external key is the + target, so a target that only the code of its unit calls is found + through the definition. `found` moves from 30 to 331 Go method targets, + from 13 to 51 Go function targets, and from 0 to 16 Rust method targets. + The share of Go method targets that end as `unknown-receiver` falls from + 0.247 to 0.119, and no target is `no-such-target` any more for Go. +- The match first gave 8 Rust targets as `not-found`. cookie calls + `rand::thread_rng` from its public `Key::generate`, but its `build.rs` + was the only root, so the library-surface roots were off. A build + script, an example or a benchmark of a Rust crate is now a helper + program. Its root stays, and it does not turn off the library-surface + roots. No target of the measure is `not-found` now. +- cookie reaches `rand::thread_rng` through the code of rand, not through + its own call `crate::secure::rand::thread_rng()`. The link step does not + read `extern crate rand;` in a submodule. The path is longer, and the + answer is the same. + + +After the fixes for the review of PR 53, the measure gives the same +numbers. No project of the corpus renames a Cargo dependency, and no +function of a dependency has two platform twins with two result types. diff --git a/test/projectgraph/testdata/receivers/go/dep/dep.go b/test/projectgraph/testdata/receivers/go/dep/dep.go new file mode 100644 index 00000000..599f9c1c --- /dev/null +++ b/test/projectgraph/testdata/receivers/go/dep/dep.go @@ -0,0 +1,34 @@ +package dep + +// Thing is a type of the dependency with methods. +type Thing struct { + Child *Thing +} + +// NewThing returns a Thing. +func NewThing() *Thing { return &Thing{} } + +// NewPair returns a Thing and an error. +func NewPair() (*Thing, error) { return &Thing{}, nil } + +// Do is the method that the receiver forms call. +func (t *Thing) Do() {} + +// Next returns the child of the Thing. +func (t *Thing) Next() *Thing { return t.Child } + +// Doer is an interface that Thing implements. +type Doer interface { + Do() +} + +// NewDoer returns a Doer that holds a Thing. +func NewDoer() Doer { return &Thing{} } + +// Runner is an interface that no type of the graph implements. +type Runner interface { + Run() +} + +// NewRunner returns a Runner of another module. +func NewRunner() Runner { return nil } diff --git a/test/projectgraph/testdata/receivers/go/dep/go.mod b/test/projectgraph/testdata/receivers/go/dep/go.mod new file mode 100644 index 00000000..59944c3b --- /dev/null +++ b/test/projectgraph/testdata/receivers/go/dep/go.mod @@ -0,0 +1,3 @@ +module example.com/dep + +go 1.22 diff --git a/test/projectgraph/testdata/receivers/rust/dep/Cargo.toml b/test/projectgraph/testdata/receivers/rust/dep/Cargo.toml new file mode 100644 index 00000000..537b4e47 --- /dev/null +++ b/test/projectgraph/testdata/receivers/rust/dep/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "dep" +version = "1.0.0" +edition = "2021" diff --git a/test/projectgraph/testdata/receivers/rust/dep/src/lib.rs b/test/projectgraph/testdata/receivers/rust/dep/src/lib.rs new file mode 100644 index 00000000..33158560 --- /dev/null +++ b/test/projectgraph/testdata/receivers/rust/dep/src/lib.rs @@ -0,0 +1,55 @@ +pub struct Thing { + items: Vec, +} + +impl Thing { + pub fn new() -> Self { + Thing { items: Vec::new() } + } + + pub fn from_vec(items: Vec) -> Self { + Thing { items } + } + + pub fn insert(&mut self, item: A) { + self.items.push(item); + } + + pub fn child(&self) -> &Thing { + self + } +} + +impl Clone for Thing { + fn clone(&self) -> Self { + Thing { items: self.items.clone() } + } +} + +pub fn new_thing() -> Thing { + Thing::new() +} + +pub fn try_thing() -> Option> { + Some(Thing::new()) +} + +pub fn load_thing() -> Result, String> { + Ok(Thing::new()) +} + +pub trait Doer { + fn run(&self); +} + +impl Doer for Thing { + fn run(&self) {} +} + +pub fn doer() -> Box { + Box::new(Thing::::new()) +} + +pub trait Runner { + fn start(&self); +} diff --git a/test/projectgraph/version_test.go b/test/projectgraph/version_test.go index c8756b6c..4fd1d2e5 100644 --- a/test/projectgraph/version_test.go +++ b/test/projectgraph/version_test.go @@ -24,7 +24,13 @@ import ( var graphHashes = map[string]string{ "10/10": "fa32ae11c72bdf01c93d4b7afe3564b1d59b2436306e3b3503f01fc4c308e5aa", "11/10": "421c6441093c8f82e4db8a2342884b5e42faf360951985d3600d99c96202625b", - "11/11": "0638d53222abbdbdbe81e09f4cdc3fb8f49b644e3576a7bfec2bfe68807b6392", + "11/11": "19e0527969cf2702f5abfab8234310b254ac25a6a539ab6e86cf6febabfb8757", + "11/12": "14d5bbbaca07ab950f75c665f48e71ce9195da2ebd853cd1b0b745d42c4852cc", + "12/12": "1cb170ec83fe3517e6f71755983c040210f83bb7a0e69528b3d7007dcc69d5dc", + "13/13": "9383a0bc5c6e8b760beb443112cae1400c39212c1da5c2d155c22488eb00eaa6", + "14/14": "0f3c9c38b39f4de1835f34e8a58bcea85461f37e7aa2d5a2dd484569e670ff48", + "14/15": "bbc708aba767ccf554c75a7e06816abda3fc1804ad6d0876ad8ce9575f4c3a88", + "14/16": "823b1f82c4aed8ba27d19275505c8d1dee755086be48fcda64d323f55b4c62a5", } // resultHashes records the hash of the reach results of every case for @@ -33,7 +39,13 @@ var graphHashes = map[string]string{ var resultHashes = map[string]string{ "11/10/1": "4beb6248ef1b8dc3572939a41d1aea11c6504f5007a5f24fd1261db026708186", "11/11/1": "4beb6248ef1b8dc3572939a41d1aea11c6504f5007a5f24fd1261db026708186", - "11/11/2": "2b48d96ff43fe68aa70967dab6cd7ac7b2f93c9e247b45fec0a472c869781448", + "11/11/2": "58d8fd9c5b67bf02a44778a5173828515c9424a1fddd9ff3f644d8fc13ebcfbb", + "11/12/2": "8341f2a1965f9d3b19d976ff1ba63ed26c1e15026e02079ec6195dfc0d762dba", + "12/12/2": "b89a142078d885eeb73fe15c731315c60d42b38270efd12a7857be44b9ccc83c", + "13/13/2": "2f81b00e1708cbb7e0589f34eaa7866a5658a6cfc0a7608d532381ca4267ba06", + "14/14/2": "a832ea709bf3df852814375a56e6329c35e2ff4c49ae34ee49afe042e9502d17", + "14/15/3": "77dfd0ea8ab71869ef612d41b8c2e46852d4fb67a49458cb645321692af77ea0", + "14/16/3": "13b6eecb686ddec3c2ba470fc9752ea66cff445d5e18e0772a71b15ae7fe1c9c", } // summaryHashes records the hash of the golden summaries of the cases for @@ -44,6 +56,12 @@ var summaryHashes = map[string]string{ "11/10/1": "f3c6c7e7f2390a0c8ec81ab6673b6b8a02cadf95e0ca36dc963d57b72834589c", "11/10/2": "8047cc4e4d95ce6744993a20c7f4224a78b2d3531c2e31415f371b16608f19a5", "11/11/2": "c0aeb7f515b2c546fd6b355663d5a7bb446c7f6bdfb9ab3ef343dd252e4302bf", + "11/12/2": "f4102bd40a876fa387f7ff1ce81505e1f04b64cd8165602d0726f897e2b88a7f", + "12/12/2": "cb2147fda018110f38254519bbc75a814b99e0318f5c7485fbce78a667bb4287", + "13/13/2": "dc844b1cd13e7873f54d646f27384c78b64707e13ae9c9ef27a4557ba7ba4dfc", + "14/14/2": "27424c4e2525450479faba78421959738748811d70b9a93b81e2bbdf1733bf0d", + "14/15/2": "5969f3cda15b666cbd902bbc2db953cbdef8c0ce48d5e77446cf243cdd0bb0e7", + "14/16/2": "fb678202978212aae932e50a56a0d3dc7f0f794ce908988bf605ce9a6e04bc51", } func TestGraphHashFollowsVersion(t *testing.T) {