Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sort ordering and type alias resolution #8438

Merged
merged 4 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@
###########
/src/dotnet/Azure.ClientSdk.Analyzers @jsquire @pallavit @JoshLove-msft @christothes @annelo-msft @KrzysztofCwalina @tg-msft @heaths @m-nash
/src/dotnet/APIView @chidozieononiwu

###########
# Go Client Tools
###########
/src/go @jhendrixMSFT @chlowell @RickWinter
34 changes: 34 additions & 0 deletions src/go/cmd/api_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"slices"
"sort"
"strings"
)
Expand Down Expand Up @@ -74,6 +75,22 @@ func createReview(pkgDir string) (PackageReview, error) {
})
diagnostics = append(diagnostics, p.diagnostics...)
}

slices.SortFunc(diagnostics, func(a Diagnostic, b Diagnostic) int {
targetCmp := strings.Compare(a.TargetID, b.TargetID)
if targetCmp != 0 {
return targetCmp
}
// if the target IDs are the same then fall back to the text.
// this accounts for cases where there are multiple diagnostics
// for the same target ID.
return strings.Compare(a.Text, b.Text)
})

for _, n := range nav {
recursiveSortNavigation(n)
}

return PackageReview{
Diagnostics: diagnostics,
Language: "Go",
Expand All @@ -83,3 +100,20 @@ func createReview(pkgDir string) (PackageReview, error) {
PackageName: m.PackageName,
}, nil
}

func recursiveSortNavigation(n Navigation) {
for _, nn := range n.ChildItems {
recursiveSortNavigation(nn)
}
slices.SortFunc(n.ChildItems, func(a Navigation, b Navigation) int {
aa, err := json.Marshal(a)
chlowell marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
panic(err)
}
bb, err := json.Marshal(b)
if err != nil {
panic(err)
}
return strings.Compare(string(aa), string(bb))
})
}
17 changes: 17 additions & 0 deletions src/go/cmd/api_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd

import (
"encoding/json"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -298,3 +299,19 @@ func Test_getPackageNameFromModPath(t *testing.T) {
require.EqualValues(t, "sdk/foo/bar", getPackageNameFromModPath("github.com/Azure/azure-sdk-for-go/sdk/foo/bar"))
require.EqualValues(t, "sdk/foo/bar", getPackageNameFromModPath("github.com/Azure/azure-sdk-for-go/sdk/foo/bar/v5"))
}

func TestDeterministicOutput(t *testing.T) {
jhendrixMSFT marked this conversation as resolved.
Show resolved Hide resolved
for i := 0; i < 100; i++ {
review1, err := createReview(filepath.Clean("testdata/test_multi_recursive_alias"))
require.NoError(t, err)
review2, err := createReview(filepath.Clean("testdata/test_multi_recursive_alias"))
require.NoError(t, err)

output1, err := json.MarshalIndent(review1, "", " ")
require.NoError(t, err)
output2, err := json.MarshalIndent(review2, "", " ")
require.NoError(t, err)

require.EqualValues(t, string(output1), string(output2))
}
}
174 changes: 97 additions & 77 deletions src/go/cmd/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func NewModule(dir string) (*Module, error) {

packageName := getPackageNameFromModPath(mf.Module.Mod.Path)
fmt.Printf("Package Name: %s\n", packageName)
m := Module{Name: filepath.Base(dir), PackageName: packageName, packages: map[string]*Pkg{}}
m := &Module{Name: filepath.Base(dir), PackageName: packageName, packages: map[string]*Pkg{}}

baseImportPath := path.Dir(mf.Module.Mod.Path) + "/"
if baseImportPath == "./" {
Expand Down Expand Up @@ -114,94 +114,114 @@ func NewModule(dir string) (*Module, error) {
// the definition from azcore/internal/shared into the APIView for azcore, making the type's
// fields visible there.
externalPackages := map[string]*Pkg{}

// tracks which packages have had their type aliases resolved.
// this prevents resolving dependent packages multiple times.
processedPackages := map[string]struct{}{}

for _, p := range m.packages {
for alias, qn := range p.typeAliases {
jhendrixMSFT marked this conversation as resolved.
Show resolved Hide resolved
// qn is a type name qualified with import path like
// "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared.TokenRequestOptions"
impPath := qn[:strings.LastIndex(qn, ".")]
typeName := qn[len(impPath)+1:]
var source *Pkg
var ok bool
if source, ok = m.packages[impPath]; !ok {
// must be a package external to this module
if source, ok = externalPackages[impPath]; !ok && sdkRoot != "" {
// figure out a path to the package, index it
if _, after, found := strings.Cut(impPath, "azure-sdk-for-go/sdk/"); found {
p := filepath.Join(sdkRoot, strings.TrimSuffix(versionReg.ReplaceAllString(after, "/"), "/"))
pkg, err := NewPkg(p, "github.com/Azure/azure-sdk-for-go/sdk/"+after)
if err == nil {
pkg.Index()
externalPackages[impPath] = pkg
source = pkg
} else {
// types from this module will appear in the review without their definitions
fmt.Printf("couldn't parse %s: %v\n", impPath, err)
}
recursiveResolveTypeAliases(m, p, externalPackages, sdkRoot, processedPackages)
}
return m, nil
}

func recursiveResolveTypeAliases(m *Module, p *Pkg, externalPackages map[string]*Pkg, sdkRoot string, processedPackages map[string]struct{}) {
if _, ok := processedPackages[p.relName]; ok {
// already processed this package
return
}

for alias, qn := range p.typeAliases {
// qn is a type name qualified with import path like
// "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared.TokenRequestOptions"
impPath := qn[:strings.LastIndex(qn, ".")]
typeName := qn[len(impPath)+1:]
var source *Pkg
var ok bool
if source, ok = m.packages[impPath]; !ok {
// must be a package external to this module
if source, ok = externalPackages[impPath]; !ok && sdkRoot != "" {
// figure out a path to the package, index it
if _, after, found := strings.Cut(impPath, "azure-sdk-for-go/sdk/"); found {
p := filepath.Join(sdkRoot, strings.TrimSuffix(versionReg.ReplaceAllString(after, "/"), "/"))
pkg, err := NewPkg(p, "github.com/Azure/azure-sdk-for-go/sdk/"+after)
if err == nil {
pkg.Index()
externalPackages[impPath] = pkg
source = pkg
} else {
// types from this module will appear in the review without their definitions
fmt.Printf("couldn't parse %s: %v\n", impPath, err)
}
}
}
} else if len(source.typeAliases) > 0 {
// if the source has type aliases we need to resolve them first.
// this is to handle recursive type aliases.
recursiveResolveTypeAliases(m, source, externalPackages, sdkRoot, processedPackages)
}

level := DiagnosticLevelInfo
originalName := qn
if _, after, found := strings.Cut(qn, m.Name); found {
originalName = strings.TrimPrefix(after, "/")
} else {
// this type is defined in another module
level = DiagnosticLevelWarning
}
level := DiagnosticLevelInfo
originalName := qn
if _, after, found := strings.Cut(qn, m.Name); found {
originalName = strings.TrimPrefix(after, "/")
} else {
// this type is defined in another module
level = DiagnosticLevelWarning
}

var t TokenMaker
if source == nil {
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
} else if def, ok := recursiveFindTypeDef(typeName, source, m.packages); ok {
switch n := def.n.Type.(type) {
case *ast.InterfaceType:
t = p.c.addInterface(*def.p, alias, p.Name(), n, nil)
case *ast.StructType:
t = p.c.addStruct(*def.p, alias, p.Name(), def.n, nil)
hoistMethodsForType(source, alias, p)
// ensure that all struct field types that are structs are also aliased from this package
for _, field := range n.Fields.List {
fieldTypeName := unwrapStructFieldTypeName(field)
if fieldTypeName == "" {
// we can ignore this field
continue
}

// ensure that our package exports this type
if _, ok := p.typeAliases[fieldTypeName]; ok {
// found an alias
continue
}

// no alias, add a diagnostic
p.diagnostics = append(p.diagnostics, Diagnostic{
Level: DiagnosticLevelError,
TargetID: t.ID(),
Text: missingAliasFor + fieldTypeName,
})
var t TokenMaker
if source == nil {
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
} else if def, ok := recursiveFindTypeDef(typeName, source, m.packages); ok {
switch n := def.n.Type.(type) {
case *ast.InterfaceType:
t = p.c.addInterface(*def.p, alias, p.Name(), n, nil)
case *ast.StructType:
t = p.c.addStruct(*def.p, alias, p.Name(), def.n, nil)
hoistMethodsForType(source, alias, p)
// ensure that all struct field types that are structs are also aliased from this package
for _, field := range n.Fields.List {
fieldTypeName := unwrapStructFieldTypeName(field)
if fieldTypeName == "" {
// we can ignore this field
continue
}
case *ast.Ident:
t = p.c.addSimpleType(*p, alias, p.Name(), def.n.Type.(*ast.Ident).Name, nil)
hoistMethodsForType(source, alias, p)
default:
fmt.Printf("unexpected node type %T\n", def.n.Type)
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)

// ensure that our package exports this type
if _, ok := p.typeAliases[fieldTypeName]; ok {
// found an alias
continue
}

// no alias, add a diagnostic
p.diagnostics = append(p.diagnostics, Diagnostic{
Level: DiagnosticLevelError,
TargetID: t.ID(),
Text: missingAliasFor + fieldTypeName,
})
}
} else {
fmt.Println("found no definition for " + qn)
case *ast.Ident:
t = p.c.addSimpleType(*p, alias, p.Name(), def.n.Type.(*ast.Ident).Name, nil)
hoistMethodsForType(source, alias, p)
default:
fmt.Printf("unexpected node type %T\n", def.n.Type)
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
}
} else {
fmt.Println("found no definition for " + qn)
}

if t != nil {
p.diagnostics = append(p.diagnostics, Diagnostic{
Level: level,
TargetID: t.ID(),
Text: aliasFor + originalName,
})
}
if t != nil {
p.diagnostics = append(p.diagnostics, Diagnostic{
Level: level,
TargetID: t.ID(),
Text: aliasFor + originalName,
})
}
}
return &m, nil

processedPackages[p.relName] = struct{}{}
}

// returns the type name for the specified struct field.
Expand Down
3 changes: 2 additions & 1 deletion src/go/cmd/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import (
"go/ast"
"go/parser"
"go/token"
"golang.org/x/exp/slices"
"os"
"path"
"path/filepath"
"strings"
"unicode"

"golang.org/x/exp/slices"
)

// diagnostic messages
Expand Down
3 changes: 3 additions & 0 deletions src/go/cmd/testdata/test_multi_recursive_alias/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias

go 1.18
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package exported

type Type1 struct {
Foo string
}

type Type2 struct {
Foo string
}

type Type3 struct {
Foo string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pkga

import "github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias/pkgb"

type Type1 = pkgb.Type1

type Type2 = pkgb.Type2

type Type3 = pkgb.Type3
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pkgb

import "github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias/pkgc"

type Type1 = pkgc.Type1

type Type2 = pkgc.Type2

type Type3 = pkgc.Type3
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pkgc

import "github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias/internal/exported"

type Type1 = exported.Type1

type Type2 = exported.Type2

type Type3 = exported.Type3
6 changes: 3 additions & 3 deletions src/go/go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module apiviewgo

go 1.18
go 1.21

require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
golang.org/x/mod v0.16.0
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8
golang.org/x/mod v0.18.0
)

require (
Expand Down
8 changes: 4 additions & 4 deletions src/go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
Loading