Skip to content

Commit 044a1fa

Browse files
authored
Fix sort ordering and type alias resolution (#8438)
* Fix sort ordering and type alias resolution Sort diagnostics and navigation slices. This requires a custom JSON marshaler for Navigation as it contains a map and the default behavior is non-deterministic. Recursively resolve type aliases. This is to handle cases where a type alias is to another type alias. * remove custom JSON marshaler according to the docs, the keys *are* sorted * add unit test * add code owners for Go APIView tool
1 parent 72ee2e1 commit 044a1fa

12 files changed

Lines changed: 205 additions & 85 deletions

File tree

.github/CODEOWNERS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,8 @@
5555
###########
5656
/src/dotnet/Azure.ClientSdk.Analyzers @jsquire @pallavit @JoshLove-msft @christothes @annelo-msft @KrzysztofCwalina @tg-msft @heaths @m-nash
5757
/src/dotnet/APIView @chidozieononiwu
58+
59+
###########
60+
# Go Client Tools
61+
###########
62+
/src/go @jhendrixMSFT @chlowell @RickWinter

src/go/cmd/api_view.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"os"
99
"path/filepath"
10+
"slices"
1011
"sort"
1112
"strings"
1213
)
@@ -74,6 +75,22 @@ func createReview(pkgDir string) (PackageReview, error) {
7475
})
7576
diagnostics = append(diagnostics, p.diagnostics...)
7677
}
78+
79+
slices.SortFunc(diagnostics, func(a Diagnostic, b Diagnostic) int {
80+
targetCmp := strings.Compare(a.TargetID, b.TargetID)
81+
if targetCmp != 0 {
82+
return targetCmp
83+
}
84+
// if the target IDs are the same then fall back to the text.
85+
// this accounts for cases where there are multiple diagnostics
86+
// for the same target ID.
87+
return strings.Compare(a.Text, b.Text)
88+
})
89+
90+
for _, n := range nav {
91+
recursiveSortNavigation(n)
92+
}
93+
7794
return PackageReview{
7895
Diagnostics: diagnostics,
7996
Language: "Go",
@@ -83,3 +100,20 @@ func createReview(pkgDir string) (PackageReview, error) {
83100
PackageName: m.PackageName,
84101
}, nil
85102
}
103+
104+
func recursiveSortNavigation(n Navigation) {
105+
for _, nn := range n.ChildItems {
106+
recursiveSortNavigation(nn)
107+
}
108+
slices.SortFunc(n.ChildItems, func(a Navigation, b Navigation) int {
109+
aa, err := json.Marshal(a)
110+
if err != nil {
111+
panic(err)
112+
}
113+
bb, err := json.Marshal(b)
114+
if err != nil {
115+
panic(err)
116+
}
117+
return strings.Compare(string(aa), string(bb))
118+
})
119+
}

src/go/cmd/api_view_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package cmd
55

66
import (
7+
"encoding/json"
78
"os"
89
"path/filepath"
910
"testing"
@@ -298,3 +299,19 @@ func Test_getPackageNameFromModPath(t *testing.T) {
298299
require.EqualValues(t, "sdk/foo/bar", getPackageNameFromModPath("github.com/Azure/azure-sdk-for-go/sdk/foo/bar"))
299300
require.EqualValues(t, "sdk/foo/bar", getPackageNameFromModPath("github.com/Azure/azure-sdk-for-go/sdk/foo/bar/v5"))
300301
}
302+
303+
func TestDeterministicOutput(t *testing.T) {
304+
for i := 0; i < 100; i++ {
305+
review1, err := createReview(filepath.Clean("testdata/test_multi_recursive_alias"))
306+
require.NoError(t, err)
307+
review2, err := createReview(filepath.Clean("testdata/test_multi_recursive_alias"))
308+
require.NoError(t, err)
309+
310+
output1, err := json.MarshalIndent(review1, "", " ")
311+
require.NoError(t, err)
312+
output2, err := json.MarshalIndent(review2, "", " ")
313+
require.NoError(t, err)
314+
315+
require.EqualValues(t, string(output1), string(output2))
316+
}
317+
}

src/go/cmd/module.go

Lines changed: 97 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func NewModule(dir string) (*Module, error) {
7171

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

7676
baseImportPath := path.Dir(mf.Module.Mod.Path) + "/"
7777
if baseImportPath == "./" {
@@ -114,94 +114,114 @@ func NewModule(dir string) (*Module, error) {
114114
// the definition from azcore/internal/shared into the APIView for azcore, making the type's
115115
// fields visible there.
116116
externalPackages := map[string]*Pkg{}
117+
118+
// tracks which packages have had their type aliases resolved.
119+
// this prevents resolving dependent packages multiple times.
120+
processedPackages := map[string]struct{}{}
121+
117122
for _, p := range m.packages {
118-
for alias, qn := range p.typeAliases {
119-
// qn is a type name qualified with import path like
120-
// "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared.TokenRequestOptions"
121-
impPath := qn[:strings.LastIndex(qn, ".")]
122-
typeName := qn[len(impPath)+1:]
123-
var source *Pkg
124-
var ok bool
125-
if source, ok = m.packages[impPath]; !ok {
126-
// must be a package external to this module
127-
if source, ok = externalPackages[impPath]; !ok && sdkRoot != "" {
128-
// figure out a path to the package, index it
129-
if _, after, found := strings.Cut(impPath, "azure-sdk-for-go/sdk/"); found {
130-
p := filepath.Join(sdkRoot, strings.TrimSuffix(versionReg.ReplaceAllString(after, "/"), "/"))
131-
pkg, err := NewPkg(p, "github.com/Azure/azure-sdk-for-go/sdk/"+after)
132-
if err == nil {
133-
pkg.Index()
134-
externalPackages[impPath] = pkg
135-
source = pkg
136-
} else {
137-
// types from this module will appear in the review without their definitions
138-
fmt.Printf("couldn't parse %s: %v\n", impPath, err)
139-
}
123+
recursiveResolveTypeAliases(m, p, externalPackages, sdkRoot, processedPackages)
124+
}
125+
return m, nil
126+
}
127+
128+
func recursiveResolveTypeAliases(m *Module, p *Pkg, externalPackages map[string]*Pkg, sdkRoot string, processedPackages map[string]struct{}) {
129+
if _, ok := processedPackages[p.relName]; ok {
130+
// already processed this package
131+
return
132+
}
133+
134+
for alias, qn := range p.typeAliases {
135+
// qn is a type name qualified with import path like
136+
// "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared.TokenRequestOptions"
137+
impPath := qn[:strings.LastIndex(qn, ".")]
138+
typeName := qn[len(impPath)+1:]
139+
var source *Pkg
140+
var ok bool
141+
if source, ok = m.packages[impPath]; !ok {
142+
// must be a package external to this module
143+
if source, ok = externalPackages[impPath]; !ok && sdkRoot != "" {
144+
// figure out a path to the package, index it
145+
if _, after, found := strings.Cut(impPath, "azure-sdk-for-go/sdk/"); found {
146+
p := filepath.Join(sdkRoot, strings.TrimSuffix(versionReg.ReplaceAllString(after, "/"), "/"))
147+
pkg, err := NewPkg(p, "github.com/Azure/azure-sdk-for-go/sdk/"+after)
148+
if err == nil {
149+
pkg.Index()
150+
externalPackages[impPath] = pkg
151+
source = pkg
152+
} else {
153+
// types from this module will appear in the review without their definitions
154+
fmt.Printf("couldn't parse %s: %v\n", impPath, err)
140155
}
141156
}
142157
}
158+
} else if len(source.typeAliases) > 0 {
159+
// if the source has type aliases we need to resolve them first.
160+
// this is to handle recursive type aliases.
161+
recursiveResolveTypeAliases(m, source, externalPackages, sdkRoot, processedPackages)
162+
}
143163

144-
level := DiagnosticLevelInfo
145-
originalName := qn
146-
if _, after, found := strings.Cut(qn, m.Name); found {
147-
originalName = strings.TrimPrefix(after, "/")
148-
} else {
149-
// this type is defined in another module
150-
level = DiagnosticLevelWarning
151-
}
164+
level := DiagnosticLevelInfo
165+
originalName := qn
166+
if _, after, found := strings.Cut(qn, m.Name); found {
167+
originalName = strings.TrimPrefix(after, "/")
168+
} else {
169+
// this type is defined in another module
170+
level = DiagnosticLevelWarning
171+
}
152172

153-
var t TokenMaker
154-
if source == nil {
155-
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
156-
} else if def, ok := recursiveFindTypeDef(typeName, source, m.packages); ok {
157-
switch n := def.n.Type.(type) {
158-
case *ast.InterfaceType:
159-
t = p.c.addInterface(*def.p, alias, p.Name(), n, nil)
160-
case *ast.StructType:
161-
t = p.c.addStruct(*def.p, alias, p.Name(), def.n, nil)
162-
hoistMethodsForType(source, alias, p)
163-
// ensure that all struct field types that are structs are also aliased from this package
164-
for _, field := range n.Fields.List {
165-
fieldTypeName := unwrapStructFieldTypeName(field)
166-
if fieldTypeName == "" {
167-
// we can ignore this field
168-
continue
169-
}
170-
171-
// ensure that our package exports this type
172-
if _, ok := p.typeAliases[fieldTypeName]; ok {
173-
// found an alias
174-
continue
175-
}
176-
177-
// no alias, add a diagnostic
178-
p.diagnostics = append(p.diagnostics, Diagnostic{
179-
Level: DiagnosticLevelError,
180-
TargetID: t.ID(),
181-
Text: missingAliasFor + fieldTypeName,
182-
})
173+
var t TokenMaker
174+
if source == nil {
175+
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
176+
} else if def, ok := recursiveFindTypeDef(typeName, source, m.packages); ok {
177+
switch n := def.n.Type.(type) {
178+
case *ast.InterfaceType:
179+
t = p.c.addInterface(*def.p, alias, p.Name(), n, nil)
180+
case *ast.StructType:
181+
t = p.c.addStruct(*def.p, alias, p.Name(), def.n, nil)
182+
hoistMethodsForType(source, alias, p)
183+
// ensure that all struct field types that are structs are also aliased from this package
184+
for _, field := range n.Fields.List {
185+
fieldTypeName := unwrapStructFieldTypeName(field)
186+
if fieldTypeName == "" {
187+
// we can ignore this field
188+
continue
183189
}
184-
case *ast.Ident:
185-
t = p.c.addSimpleType(*p, alias, p.Name(), def.n.Type.(*ast.Ident).Name, nil)
186-
hoistMethodsForType(source, alias, p)
187-
default:
188-
fmt.Printf("unexpected node type %T\n", def.n.Type)
189-
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
190+
191+
// ensure that our package exports this type
192+
if _, ok := p.typeAliases[fieldTypeName]; ok {
193+
// found an alias
194+
continue
195+
}
196+
197+
// no alias, add a diagnostic
198+
p.diagnostics = append(p.diagnostics, Diagnostic{
199+
Level: DiagnosticLevelError,
200+
TargetID: t.ID(),
201+
Text: missingAliasFor + fieldTypeName,
202+
})
190203
}
191-
} else {
192-
fmt.Println("found no definition for " + qn)
204+
case *ast.Ident:
205+
t = p.c.addSimpleType(*p, alias, p.Name(), def.n.Type.(*ast.Ident).Name, nil)
206+
hoistMethodsForType(source, alias, p)
207+
default:
208+
fmt.Printf("unexpected node type %T\n", def.n.Type)
209+
t = p.c.addSimpleType(*p, alias, p.Name(), originalName, nil)
193210
}
211+
} else {
212+
fmt.Println("found no definition for " + qn)
213+
}
194214

195-
if t != nil {
196-
p.diagnostics = append(p.diagnostics, Diagnostic{
197-
Level: level,
198-
TargetID: t.ID(),
199-
Text: aliasFor + originalName,
200-
})
201-
}
215+
if t != nil {
216+
p.diagnostics = append(p.diagnostics, Diagnostic{
217+
Level: level,
218+
TargetID: t.ID(),
219+
Text: aliasFor + originalName,
220+
})
202221
}
203222
}
204-
return &m, nil
223+
224+
processedPackages[p.relName] = struct{}{}
205225
}
206226

207227
// returns the type name for the specified struct field.

src/go/cmd/pkg.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import (
99
"go/ast"
1010
"go/parser"
1111
"go/token"
12-
"golang.org/x/exp/slices"
1312
"os"
1413
"path"
1514
"path/filepath"
1615
"strings"
1716
"unicode"
17+
18+
"golang.org/x/exp/slices"
1819
)
1920

2021
// diagnostic messages
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias
2+
3+
go 1.18
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package exported
2+
3+
type Type1 struct {
4+
Foo string
5+
}
6+
7+
type Type2 struct {
8+
Foo string
9+
}
10+
11+
type Type3 struct {
12+
Foo string
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package pkga
2+
3+
import "github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias/pkgb"
4+
5+
type Type1 = pkgb.Type1
6+
7+
type Type2 = pkgb.Type2
8+
9+
type Type3 = pkgb.Type3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package pkgb
2+
3+
import "github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias/pkgc"
4+
5+
type Type1 = pkgc.Type1
6+
7+
type Type2 = pkgc.Type2
8+
9+
type Type3 = pkgc.Type3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package pkgc
2+
3+
import "github.com/Azure/azure-sdk-for-go/sdk/test_multi_recursive_alias/internal/exported"
4+
5+
type Type1 = exported.Type1
6+
7+
type Type2 = exported.Type2
8+
9+
type Type3 = exported.Type3

0 commit comments

Comments
 (0)