diff --git a/cmd/godex/gc.go b/cmd/godex/gc.go index 66b0a0e6..95eba658 100644 --- a/cmd/godex/gc.go +++ b/cmd/godex/gc.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 - // This file implements access to gc-generated export data. package main diff --git a/cmd/godex/gccgo.go b/cmd/godex/gccgo.go index 785441cd..7644998d 100644 --- a/cmd/godex/gccgo.go +++ b/cmd/godex/gccgo.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 - // This file implements access to gccgo-generated export data. package main diff --git a/cmd/godex/godex.go b/cmd/godex/godex.go index 5d40d878..a222ed63 100644 --- a/cmd/godex/godex.go +++ b/cmd/godex/godex.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 - package main import ( diff --git a/cmd/godex/isAlias18.go b/cmd/godex/isAlias18.go new file mode 100644 index 00000000..cab12926 --- /dev/null +++ b/cmd/godex/isAlias18.go @@ -0,0 +1,13 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.9 + +package main + +import "go/types" + +func isAlias(obj *types.TypeName) bool { + return false // there are no type aliases before Go 1.9 +} diff --git a/cmd/godex/isAlias19.go b/cmd/godex/isAlias19.go new file mode 100644 index 00000000..6ebdd42e --- /dev/null +++ b/cmd/godex/isAlias19.go @@ -0,0 +1,13 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.9 + +package main + +import "go/types" + +func isAlias(obj *types.TypeName) bool { + return obj.IsAlias() +} diff --git a/cmd/godex/print.go b/cmd/godex/print.go index 02b46060..adce8646 100644 --- a/cmd/godex/print.go +++ b/cmd/godex/print.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 - package main import ( @@ -143,7 +141,13 @@ func (p *printer) printPackage(pkg *types.Package, filter func(types.Object) boo p.printDecl("type", len(typez), func() { for _, obj := range typez { p.printf("%s ", obj.Name()) - p.writeType(p.pkg, obj.Type().Underlying()) + typ := obj.Type() + if isAlias(obj) { + p.print("= ") + p.writeType(p.pkg, typ) + } else { + p.writeType(p.pkg, typ.Underlying()) + } p.print("\n") } }) diff --git a/cmd/godex/source.go b/cmd/godex/source.go index 0d3c0068..85235e9f 100644 --- a/cmd/godex/source.go +++ b/cmd/godex/source.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 - // This file implements access to export data from source. package main diff --git a/cmd/godex/writetype.go b/cmd/godex/writetype.go index ed0bd9f2..dd17f90d 100644 --- a/cmd/godex/writetype.go +++ b/cmd/godex/writetype.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build go1.5 - // This file implements writing of types. The functionality is lifted // directly from go/types, but now contains various modifications for // nicer output. diff --git a/cmd/guru/pos.go b/cmd/guru/pos.go index 591aad8e..2e659fe4 100644 --- a/cmd/guru/pos.go +++ b/cmd/guru/pos.go @@ -52,14 +52,14 @@ func parsePos(pos string) (filename string, startOffset, endOffset int, err erro filename, offset := pos[:colon], pos[colon+1:] startOffset = -1 endOffset = -1 - if hyphen := strings.Index(offset, ","); hyphen < 0 { + if comma := strings.Index(offset, ","); comma < 0 { // e.g. "foo.go:#123" startOffset = parseOctothorpDecimal(offset) endOffset = startOffset } else { // e.g. "foo.go:#123,#456" - startOffset = parseOctothorpDecimal(offset[:hyphen]) - endOffset = parseOctothorpDecimal(offset[hyphen+1:]) + startOffset = parseOctothorpDecimal(offset[:comma]) + endOffset = parseOctothorpDecimal(offset[comma+1:]) } if startOffset < 0 || endOffset < 0 { err = fmt.Errorf("invalid offset %q in query position", offset) diff --git a/go/gcimporter15/bexport.go b/go/gcimporter15/bexport.go index af982aba..9f5db0e8 100644 --- a/go/gcimporter15/bexport.go +++ b/go/gcimporter15/bexport.go @@ -39,11 +39,12 @@ const debugFormat = false // default: false const trace = false // default: false // Current export format version. Increase with each format change. -// 3: added aliasTag and export of aliases +// 4: type name objects support type aliases, uses aliasTag +// 3: Go1.8 encoding (same as version 2, aliasTag defined but never used) // 2: removed unused bool in ODCL export (compiler only) // 1: header format change (more regular), export package for _ struct fields // 0: Go1.7 encoding -const exportVersion = 3 +const exportVersion = 4 // trackAllTypes enables cycle tracking for all types, not just named // types. The existing compiler invariants assume that unnamed types @@ -65,9 +66,6 @@ type exporter struct { pkgIndex map[*types.Package]int typIndex map[types.Type]int - // track objects that we've reexported already - reexported map[types.Object]bool - // position encoding posInfoFormat bool prevFile string @@ -86,7 +84,6 @@ func BExportData(fset *token.FileSet, pkg *types.Package) []byte { strIndex: map[string]int{"": 0}, // empty string is mapped to 0 pkgIndex: make(map[*types.Package]int), typIndex: make(map[types.Type]int), - reexported: make(map[types.Object]bool), posInfoFormat: true, // TODO(gri) might become a flag, eventually } @@ -188,7 +185,13 @@ func (p *exporter) obj(obj types.Object) { p.value(obj.Val()) case *types.TypeName: - p.tag(typeTag) + if isAlias(obj) { + p.tag(aliasTag) + p.pos(obj) + p.qualifiedName(obj) + } else { + p.tag(typeTag) + } p.typ(obj.Type()) case *types.Var: @@ -205,21 +208,6 @@ func (p *exporter) obj(obj types.Object) { p.paramList(sig.Params(), sig.Variadic()) p.paramList(sig.Results(), false) - // Alias-related code. Keep for now. - // case *types_Alias: - // // make sure the original is exported before the alias - // // (if the alias declaration was invalid, orig will be nil) - // orig := original(obj) - // if orig != nil && !p.reexported[orig] { - // p.obj(orig) - // p.reexported[orig] = true - // } - - // p.tag(aliasTag) - // p.pos(obj) - // p.string(obj.Name()) - // p.qualifiedName(orig) - default: log.Fatalf("gcimporter: unexpected object %v (%T)", obj, obj) } @@ -279,10 +267,6 @@ func commonPrefixLen(a, b string) int { } func (p *exporter) qualifiedName(obj types.Object) { - if obj == nil { - p.string("") - return - } p.string(obj.Name()) p.pkg(obj.Pkg(), false) } @@ -482,28 +466,45 @@ func (p *exporter) method(m *types.Func) { p.paramList(sig.Results(), false) } -// fieldName is like qualifiedName but it doesn't record the package for exported names. func (p *exporter) fieldName(f *types.Var) { name := f.Name() - // anonymous field with unexported base type name: use "?" as field name - // (bname != "" per spec, but we are conservative in case of errors) if f.Anonymous() { - base := f.Type() - if ptr, ok := base.(*types.Pointer); ok { - base = ptr.Elem() - } - if named, ok := base.(*types.Named); ok && !named.Obj().Exported() { - // anonymous field with unexported base type name - name = "?" // unexported name to force export of package + // anonymous field - we distinguish between 3 cases: + // 1) field name matches base type name and is exported + // 2) field name matches base type name and is not exported + // 3) field name doesn't match base type name (alias name) + bname := basetypeName(f.Type()) + if name == bname { + if ast.IsExported(name) { + name = "" // 1) we don't need to know the field name or package + } else { + name = "?" // 2) use unexported name "?" to force package export + } + } else { + // 3) indicate alias and export name as is + // (this requires an extra "@" but this is a rare case) + p.string("@") } } + p.string(name) - if !f.Exported() { + if name != "" && !ast.IsExported(name) { p.pkg(f.Pkg(), false) } } +func basetypeName(typ types.Type) string { + switch typ := deref(typ).(type) { + case *types.Basic: + return typ.Name() + case *types.Named: + return typ.Obj().Name() + default: + return "" // unnamed type + } +} + func (p *exporter) paramList(params *types.Tuple, variadic bool) { // use negative length to indicate unnamed parameters // (look at the first parameter only since either all @@ -797,10 +798,10 @@ func (p *exporter) tracef(format string, args ...interface{}) { // Debugging support. // (tagString is only used when tracing is enabled) var tagString = [...]string{ - // Packages: + // Packages -packageTag: "package", - // Types: + // Types -namedTag: "named type", -arrayTag: "array", -sliceTag: "slice", @@ -812,7 +813,7 @@ var tagString = [...]string{ -mapTag: "map", -chanTag: "chan", - // Values: + // Values -falseTag: "false", -trueTag: "true", -int64Tag: "int64", @@ -821,4 +822,7 @@ var tagString = [...]string{ -complexTag: "complex", -stringTag: "string", -unknownTag: "unknown", + + // Type aliases + -aliasTag: "alias", } diff --git a/go/gcimporter15/bexport18_test.go b/go/gcimporter15/bexport18_test.go deleted file mode 100644 index d0decdd4..00000000 --- a/go/gcimporter15/bexport18_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2016 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Alias-related code. Keep for now. -// +build ignore - -package gcimporter_test - -import ( - "go/ast" - "go/parser" - "go/token" - "go/types" - "testing" - - gcimporter "golang.org/x/tools/go/gcimporter15" -) - -func disabledTestInvalidAlias(t *testing.T) { - // parse and typecheck - const src = "package p; func InvalidAlias => foo.f" - fset1 := token.NewFileSet() - f, err := parser.ParseFile(fset1, "p.go", src, 0) - if err != nil { - t.Fatal(err) - } - var conf types.Config - pkg, err := conf.Check("p", fset1, []*ast.File{f}, nil) - if err == nil { - t.Fatal("invalid source type-checked without error") - } - if pkg == nil { - t.Fatal("nil package returned") - } - - // export - exportdata := gcimporter.BExportData(fset1, pkg) - - // import - imports := make(map[string]*types.Package) - fset2 := token.NewFileSet() - _, pkg2, err := gcimporter.BImportData(fset2, imports, exportdata, pkg.Path()) - if err != nil { - t.Fatalf("BImportData(%s): %v", pkg.Path(), err) - } - - // pkg2 must contain InvalidAlias as an invalid Alias - obj := pkg2.Scope().Lookup("InvalidAlias") - if obj == nil { - t.Fatal("InvalidAlias not found") - } - alias, ok := obj.(*types.Alias) - if !ok { - t.Fatalf("got %v; want alias", alias) - } - if alias.Type() != types.Typ[types.Invalid] || alias.Orig() != nil { - t.Fatalf("got %v (orig = %v); want invalid alias", alias, alias.Orig()) - } -} diff --git a/go/gcimporter15/bexport19_test.go b/go/gcimporter15/bexport19_test.go new file mode 100644 index 00000000..f6e9733b --- /dev/null +++ b/go/gcimporter15/bexport19_test.go @@ -0,0 +1,93 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.9 + +package gcimporter_test + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + gcimporter "golang.org/x/tools/go/gcimporter15" +) + +const src = ` +package p + +type ( + T0 = int32 + T1 = struct{} + T2 = struct{ T1 } + Invalid = foo // foo is undeclared +) +` + +func checkPkg(t *testing.T, pkg *types.Package, label string) { + T1 := types.NewStruct(nil, nil) + T2 := types.NewStruct([]*types.Var{types.NewField(0, pkg, "T1", T1, true)}, nil) + + for _, test := range []struct { + name string + typ types.Type + }{ + {"T0", types.Typ[types.Int32]}, + {"T1", T1}, + {"T2", T2}, + {"Invalid", types.Typ[types.Invalid]}, + } { + obj := pkg.Scope().Lookup(test.name) + if obj == nil { + t.Errorf("%s: %s not found", label, test.name) + continue + } + tname, _ := obj.(*types.TypeName) + if tname == nil { + t.Errorf("%s: %v not a type name", label, obj) + continue + } + if !tname.IsAlias() { + t.Errorf("%s: %v: not marked as alias", label, tname) + continue + } + if got := tname.Type(); !types.Identical(got, test.typ) { + t.Errorf("%s: %v: got %v; want %v", label, tname, got, test.typ) + } + } +} + +func TestTypeAliases(t *testing.T) { + // parse and typecheck + fset1 := token.NewFileSet() + f, err := parser.ParseFile(fset1, "p.go", src, 0) + if err != nil { + t.Fatal(err) + } + var conf types.Config + pkg1, err := conf.Check("p", fset1, []*ast.File{f}, nil) + if err == nil { + // foo in undeclared in src; we should see an error + t.Fatal("invalid source type-checked without error") + } + if pkg1 == nil { + // despite incorrect src we should see a (partially) type-checked package + t.Fatal("nil package returned") + } + checkPkg(t, pkg1, "export") + + // export + exportdata := gcimporter.BExportData(fset1, pkg1) + + // import + imports := make(map[string]*types.Package) + fset2 := token.NewFileSet() + _, pkg2, err := gcimporter.BImportData(fset2, imports, exportdata, pkg1.Path()) + if err != nil { + t.Fatalf("BImportData(%s): %v", pkg1.Path(), err) + } + checkPkg(t, pkg2, "import") +} diff --git a/go/gcimporter15/bimport.go b/go/gcimporter15/bimport.go index ce4b6872..8bff64c3 100644 --- a/go/gcimporter15/bimport.go +++ b/go/gcimporter15/bimport.go @@ -100,10 +100,10 @@ func BImportData(fset *token.FileSet, imports map[string]*types.Package, data [] // read version specific flags - extend as necessary switch p.version { - // case 4: + // case 5: // ... // fallthrough - case 3, 2, 1: + case 4, 3, 2, 1: p.debugFormat = p.rawStringln(p.rawByte()) == "debug" p.trackAllTypes = p.int() != 0 p.posInfoFormat = p.int() != 0 @@ -210,7 +210,6 @@ func (p *importer) pkg() *types.Package { } // objTag returns the tag value for each object kind. -// obj must not be a *types.Alias. func objTag(obj types.Object) int { switch obj.(type) { case *types.Const: @@ -221,7 +220,6 @@ func objTag(obj types.Object) int { return varTag case *types.Func: return funcTag - // Aliases are not exported multiple times, thus we should not see them here. default: errorf("unexpected object: %v (%T)", obj, obj) // panics panic("unreachable") @@ -239,14 +237,14 @@ func (p *importer) declare(obj types.Object) { pkg := obj.Pkg() if alt := pkg.Scope().Insert(obj); alt != nil { // This can only trigger if we import a (non-type) object a second time. - // Excluding aliases, this cannot happen because 1) we only import a package + // Excluding type aliases, this cannot happen because 1) we only import a package // once; and b) we ignore compiler-specific export data which may contain // functions whose inlined function bodies refer to other functions that // were already imported. - // However, aliases require reexporting the original object, so we need + // However, type aliases require reexporting the original type, so we need // to allow it (see also the comment in cmd/compile/internal/gc/bimport.go, // method importer.obj, switch case importing functions). - // Note that the original itself cannot be an alias. + // TODO(gri) review/update this comment once the gc compiler handles type aliases. if !sameObj(obj, alt) { errorf("inconsistent import:\n\t%v\npreviously imported as:\n\t%v\n", obj, alt) } @@ -262,6 +260,13 @@ func (p *importer) obj(tag int) { val := p.value() p.declare(types.NewConst(pos, pkg, name, typ, val)) + case aliasTag: + // TODO(gri) verify type alias hookup is correct + pos := p.pos() + pkg, name := p.qualifiedName() + typ := p.typ(nil) + p.declare(types.NewTypeName(pos, pkg, name, typ)) + case typeTag: p.typ(nil) @@ -279,19 +284,6 @@ func (p *importer) obj(tag int) { sig := types.NewSignature(nil, params, result, isddd) p.declare(types.NewFunc(pos, pkg, name, sig)) - case aliasTag: - pos := p.pos() - name := p.string() - var orig types.Object - if pkg, name := p.qualifiedName(); pkg != nil { - orig = pkg.Scope().Lookup(name) - } - // Alias-related code. Keep for now. - _ = pos - _ = name - _ = orig - // p.declare(types.NewAlias(pos, p.pkgList[0], name, orig)) - default: errorf("unexpected object tag %d", tag) } @@ -351,9 +343,7 @@ var ( func (p *importer) qualifiedName() (pkg *types.Package, name string) { name = p.string() - if name != "" { - pkg = p.pkg() - } + pkg = p.pkg() return } @@ -558,17 +548,17 @@ func (p *importer) fieldList(parent *types.Package) (fields []*types.Var, tags [ fields = make([]*types.Var, n) tags = make([]string, n) for i := range fields { - fields[i] = p.field(parent) - tags[i] = p.string() + fields[i], tags[i] = p.field(parent) } } return } -func (p *importer) field(parent *types.Package) *types.Var { +func (p *importer) field(parent *types.Package) (*types.Var, string) { pos := p.pos() - pkg, name := p.fieldName(parent) + pkg, name, alias := p.fieldName(parent) typ := p.typ(parent) + tag := p.string() anonymous := false if name == "" { @@ -580,12 +570,15 @@ func (p *importer) field(parent *types.Package) *types.Var { case *types.Named: name = typ.Obj().Name() default: - errorf("anonymous field expected") + errorf("named base type expected") } anonymous = true + } else if alias { + // anonymous field: we have an explicit name because it's an alias + anonymous = true } - return types.NewField(pos, pkg, name, typ, anonymous) + return types.NewField(pos, pkg, name, typ, anonymous), tag } func (p *importer) methodList(parent *types.Package) (methods []*types.Func) { @@ -600,45 +593,42 @@ func (p *importer) methodList(parent *types.Package) (methods []*types.Func) { func (p *importer) method(parent *types.Package) *types.Func { pos := p.pos() - pkg, name := p.fieldName(parent) + pkg, name, _ := p.fieldName(parent) params, isddd := p.paramList() result, _ := p.paramList() sig := types.NewSignature(nil, params, result, isddd) return types.NewFunc(pos, pkg, name, sig) } -func (p *importer) fieldName(parent *types.Package) (*types.Package, string) { - name := p.string() - pkg := parent +func (p *importer) fieldName(parent *types.Package) (pkg *types.Package, name string, alias bool) { + name = p.string() + pkg = parent if pkg == nil { // use the imported package instead pkg = p.pkgList[0] } if p.version == 0 && name == "_" { // version 0 didn't export a package for _ fields - // see issue #15514 - - // For bug-compatibility with gc, pretend all imported - // blank fields belong to the same dummy package. - // This avoids spurious "cannot assign A to B" errors - // from go/types caused by types changing as they are - // re-exported. - const blankpkg = "<_>" - pkg := p.imports[blankpkg] - if pkg == nil { - pkg = types.NewPackage(blankpkg, blankpkg) - p.imports[blankpkg] = pkg - } - - return pkg, name + return } - if name != "" && !exported(name) { - if name == "?" { - name = "" - } + switch name { + case "": + // 1) field name matches base type name and is exported: nothing to do + case "?": + // 2) field name matches base type name and is not exported: need package + name = "" pkg = p.pkg() + case "@": + // 3) field name doesn't match type name (alias) + name = p.string() + alias = true + fallthrough + default: + if !exported(name) { + pkg = p.pkg() + } } - return pkg, name + return } func (p *importer) paramList() (*types.Tuple, bool) { @@ -909,7 +899,7 @@ const ( nilTag // only used by gc (appears in exported inlined function bodies) unknownTag // not used by gc (only appears in packages with errors) - // Aliases + // Type aliases aliasTag ) @@ -933,7 +923,7 @@ var predeclared = []types.Type{ types.Typ[types.Complex128], types.Typ[types.String], - // aliases + // basic type aliases types.Universe.Lookup("byte").Type(), types.Universe.Lookup("rune").Type(), diff --git a/go/gcimporter15/isAlias18.go b/go/gcimporter15/isAlias18.go new file mode 100644 index 00000000..225ffeed --- /dev/null +++ b/go/gcimporter15/isAlias18.go @@ -0,0 +1,13 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.9 + +package gcimporter + +import "go/types" + +func isAlias(obj *types.TypeName) bool { + return false // there are no type aliases before Go 1.9 +} diff --git a/go/gcimporter15/isAlias19.go b/go/gcimporter15/isAlias19.go new file mode 100644 index 00000000..c2025d84 --- /dev/null +++ b/go/gcimporter15/isAlias19.go @@ -0,0 +1,13 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.9 + +package gcimporter + +import "go/types" + +func isAlias(obj *types.TypeName) bool { + return obj.IsAlias() +} diff --git a/go/internal/gccgoimporter/importer19_test.go b/go/internal/gccgoimporter/importer19_test.go new file mode 100644 index 00000000..0caa2256 --- /dev/null +++ b/go/internal/gccgoimporter/importer19_test.go @@ -0,0 +1,15 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.9 + +package gccgoimporter + +var aliasTests = []importerTest{ + {pkgpath: "alias", name: "IntAlias2", want: "type IntAlias2 = Int"}, +} + +func init() { + importerTests = append(importerTests, aliasTests...) +} diff --git a/go/internal/gccgoimporter/importer_test.go b/go/internal/gccgoimporter/importer_test.go index 2749409c..87b5ff2a 100644 --- a/go/internal/gccgoimporter/importer_test.go +++ b/go/internal/gccgoimporter/importer_test.go @@ -4,7 +4,9 @@ package gccgoimporter -// This is a verbatim copy of $GOROOT/src/go/internal/gccgoimporter/importer_test.go. +// This is a verbatim copy of $GOROOT/src/go/internal/gccgoimporter/importer_test.go +// except for the importerTests variable which does not contain Go1.9-specific tests. +// Those are added via importer19_test.go. import ( "go/types" @@ -90,7 +92,7 @@ func runImporterTest(t *testing.T, imp Importer, initmap map[*types.Package]Init } } -var importerTests = [...]importerTest{ +var importerTests = []importerTest{ {pkgpath: "pointer", name: "Int8Ptr", want: "type Int8Ptr *int8"}, {pkgpath: "complexnums", name: "NN", want: "const NN untyped complex", wantval: "(-1 + -1i)"}, {pkgpath: "complexnums", name: "NP", want: "const NP untyped complex", wantval: "(-1 + 1i)"}, diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index ef59b298..7d0e39ce 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -372,27 +372,41 @@ func (p *parser) parseConst(pkg *types.Package) *types.Const { return types.NewConst(token.NoPos, pkg, name, typ, val) } -// TypeName = ExportedName . -func (p *parser) parseTypeName() *types.TypeName { - pkg, name := p.parseExportedName() - scope := pkg.Scope() - if obj := scope.Lookup(name); obj != nil { - return obj.(*types.TypeName) - } - obj := types.NewTypeName(token.NoPos, pkg, name, nil) - // a named type may be referred to before the underlying type - // is known - set it up - types.NewNamed(obj, nil, nil) - scope.Insert(obj) - return obj -} - -// NamedType = TypeName Type { Method } . +// NamedType = TypeName [ "=" ] Type { Method } . +// TypeName = ExportedName . // Method = "func" "(" Param ")" Name ParamList ResultList ";" . func (p *parser) parseNamedType(n int) types.Type { - obj := p.parseTypeName() + pkg, name := p.parseExportedName() + scope := pkg.Scope() + + if p.tok == '=' { + // type alias + p.next() + typ := p.parseType(pkg) + if obj := scope.Lookup(name); obj != nil { + typ = obj.Type() // use previously imported type + if typ == nil { + p.errorf("%v (type alias) used in cycle", obj) + } + } else { + obj = types.NewTypeName(token.NoPos, pkg, name, typ) + scope.Insert(obj) + } + p.typeMap[n] = typ + return typ + } + + // named type + obj := scope.Lookup(name) + if obj == nil { + // a named type may be referred to before the underlying type + // is known - set it up + tname := types.NewTypeName(token.NoPos, pkg, name, nil) + types.NewNamed(tname, nil, nil) + scope.Insert(tname) + obj = tname + } - pkg := obj.Pkg() typ := obj.Type() p.typeMap[n] = typ @@ -411,8 +425,8 @@ func (p *parser) parseNamedType(n int) types.Type { nt.SetUnderlying(underlying.Underlying()) } + // collect associated methods for p.tok == scanner.Ident { - // collect associated methods p.expectKeyword("func") p.expect('(') receiver, _ := p.parseParam(pkg) diff --git a/godoc/godoc.go b/godoc/godoc.go index 8bda89ad..1063244a 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -233,24 +233,28 @@ func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructTy if st.Fields == nil { return } - var scratch bytes.Buffer + // needsLink is a set of identifiers that still need to be + // linked, where value == key, to avoid an allocation in func + // linkedField. + needsLink := make(map[string]string) + for _, f := range st.Fields.List { if len(f.Names) == 0 { continue } fieldName := f.Names[0].Name - scratch.Reset() - var added bool - foreachLine(buf.Bytes(), func(line []byte) { - if !added && isLineForStructFieldID(line, fieldName) { - added = true - fmt.Fprintf(&scratch, ``, name, fieldName) - } - scratch.Write(line) - }) - buf.Reset() - buf.Write(scratch.Bytes()) + needsLink[fieldName] = fieldName } + var newBuf bytes.Buffer + foreachLine(buf.Bytes(), func(line []byte) { + if fieldName := linkedField(line, needsLink); fieldName != "" { + fmt.Fprintf(&newBuf, ``, name, fieldName) + delete(needsLink, fieldName) + } + newBuf.Write(line) + }) + buf.Reset() + buf.Write(newBuf.Bytes()) } // foreachLine calls fn for each line of in, where a line includes @@ -270,9 +274,12 @@ func foreachLine(in []byte, fn func(line []byte)) { // commentPrefix is the line prefix for comments after they've been HTMLified. var commentPrefix = []byte(`// `) -// isLineForStructFieldID reports whether line is a line we should -// add a to. Only the fieldName is provided. -func isLineForStructFieldID(line []byte, fieldName string) bool { +// linkedField determines whether the given line starts with an +// identifer in the provided ids map (mapping from identifier to the +// same identifier). The line can start with either an identifier or +// an identifier in a comment. If one matches, it returns the +// identifier that matched. Otherwise it returns the empty string. +func linkedField(line []byte, ids map[string]string) string { line = bytes.TrimSpace(line) // For fields with a doc string of the @@ -292,13 +299,39 @@ func isLineForStructFieldID(line []byte, fieldName string) bool { // // TODO: do this better, so it works for all // comments, including unconventional ones. - // For comments if bytes.HasPrefix(line, commentPrefix) { - if matchesIdentBoundary(line[len(commentPrefix):], fieldName) { - return true - } + line = line[len(commentPrefix):] } - return matchesIdentBoundary(line, fieldName) + id := scanIdentifier(line) + if len(id) == 0 { + // No leading identifier. Avoid map lookup for + // somewhat common case. + return "" + } + return ids[string(id)] +} + +// scanIdentifier scans a valid Go identifier off the front of v and +// either returns a subslice of v if there's a valid identifier, or +// returns a zero-length slice. +func scanIdentifier(v []byte) []byte { + var n int // number of leading bytes of v belonging to an identifier + for { + r, width := utf8.DecodeRune(v[n:]) + if !(isLetter(r) || n > 0 && isDigit(r)) { + break + } + n += width + } + return v[:n] +} + +func isLetter(ch rune) bool { + return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch) +} + +func isDigit(ch rune) bool { + return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) } // matchesIdentBoundary reports whether line matches /^ident\b/. diff --git a/godoc/godoc_test.go b/godoc/godoc_test.go index 51d41b3a..011eca33 100644 --- a/godoc/godoc_test.go +++ b/godoc/godoc_test.go @@ -5,6 +5,8 @@ package godoc import ( + "bytes" + "fmt" "go/ast" "go/parser" "go/token" @@ -123,10 +125,7 @@ func TestSanitizeFunc(t *testing.T) { // Test that we add elements // to the HTML of struct fields. func TestStructFieldsIDAttributes(t *testing.T) { - p := &Presentation{ - DeclLinks: true, - } - src := []byte(` + got := linkifyStructFields(t, []byte(` package foo type T struct { @@ -137,8 +136,32 @@ type T struct { // Opt, if non-nil, is an option. Opt *int + + // Опция - другое поле. + Опция bool } -`) +`)) + want := `type T struct { +NoDoc string + +// Doc has a comment. +Doc string + +// Opt, if non-nil, is an option. +Opt *int + +// Опция - другое поле. +Опция bool +}` + if got != want { + t.Errorf("got: %s\n\nwant: %s\n", got, want) + } +} + +func linkifyStructFields(t *testing.T, src []byte) string { + p := &Presentation{ + DeclLinks: true, + } fset := token.NewFileSet() af, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments) if err != nil { @@ -148,17 +171,46 @@ type T struct { pi := &PageInfo{ FSet: fset, } - got := p.node_htmlFunc(pi, genDecl, true) - want := `type T struct { -NoDoc string + return p.node_htmlFunc(pi, genDecl, true) +} -// Doc has a comment. -Doc string - -// Opt, if non-nil, is an option. -Opt *int -}` - if got != want { - t.Errorf("got: %s\n\nwant: %s\n", got, want) +// Verify that scanIdentifier isn't quadratic. +// This doesn't actually measure and fail on its own, but it was previously +// very obvious when running by hand. +// +// TODO: if there's a reliable and non-flaky way to test this, do so. +// Maybe count user CPU time instead of wall time? But that's not easy +// to do portably in Go. +func TestStructField(t *testing.T) { + for _, n := range []int{10, 100, 1000, 10000} { + n := n + t.Run(fmt.Sprint(n), func(t *testing.T) { + var buf bytes.Buffer + fmt.Fprintf(&buf, "package foo\n\ntype T struct {\n") + for i := 0; i < n; i++ { + fmt.Fprintf(&buf, "\t// Field%d is foo.\n\tField%d int\n\n", i, i) + } + fmt.Fprintf(&buf, "}\n") + linkifyStructFields(t, buf.Bytes()) + }) + } +} + +func TestScanIdentifier(t *testing.T) { + tests := []struct { + in, want string + }{ + {"foo bar", "foo"}, + {"foo/bar", "foo"}, + {" foo", ""}, + {"фоо", "фоо"}, + {"f123", "f123"}, + {"123f", ""}, + } + for _, tt := range tests { + got := scanIdentifier([]byte(tt.in)) + if string(got) != tt.want { + t.Errorf("scanIdentifier(%q) = %q; want %q", tt.in, got, tt.want) + } } } diff --git a/godoc/static/godocs.js b/godoc/static/godocs.js index 1fb8b91e..23a580e5 100644 --- a/godoc/static/godocs.js +++ b/godoc/static/godocs.js @@ -247,7 +247,7 @@ function fixFocus() { function toggleHash() { // Open all of the toggles for a particular hash. var els = $(document.getElementById(window.location.hash.substring(1)), - $("a[name='" + window.location.hash.substring(1) + "']")); + $.find("a[name='" + window.location.hash.substring(1) + "']")); while (els.length) { for (var i = 0; i < els.length; i++) { var el = $(els[i]); @@ -263,7 +263,14 @@ function personalizeInstallInstructions() { var prefix = '?download='; var s = window.location.search; if (s.indexOf(prefix) != 0) { - // No 'download' query string; bail. + // No 'download' query string; detect "test" instructions from User Agent. + if (navigator.platform.indexOf('Win') != -1) { + $('.testUnix').hide(); + $('.testWindows').show(); + } else { + $('.testUnix').show(); + $('.testWindows').hide(); + } return; } diff --git a/godoc/static/static.go b/godoc/static/static.go index a47a7fd0..6fab8da3 100644 --- a/godoc/static/static.go +++ b/godoc/static/static.go @@ -833,7 +833,14 @@ function personalizeInstallInstructions() { var prefix = '?download='; var s = window.location.search; if (s.indexOf(prefix) != 0) { - // No 'download' query string; bail. + // No 'download' query string; detect "test" instructions from User Agent. + if (navigator.platform.indexOf('Win') != -1) { + $('.testUnix').hide(); + $('.testWindows').show(); + } else { + $('.testUnix').show(); + $('.testWindows').hide(); + } return; } diff --git a/imports/imports.go b/imports/imports.go index c26c1946..67573f49 100644 --- a/imports/imports.go +++ b/imports/imports.go @@ -276,7 +276,7 @@ func addImportSpaces(r io.Reader, breaks []string) []byte { } if inImports && len(breaks) > 0 { if m := impLine.FindStringSubmatch(s); m != nil { - if m[1] == string(breaks[0]) { + if m[1] == breaks[0] { out.WriteByte('\n') breaks = breaks[1:] } diff --git a/present/iframe.go b/present/iframe.go index 2f3c5e55..46649f0f 100644 --- a/present/iframe.go +++ b/present/iframe.go @@ -39,7 +39,7 @@ func parseIframe(ctx *Context, fileName string, lineno int, text string) (Elem, i.Width = v } default: - return nil, fmt.Errorf("incorrect image invocation: %q", text) + return nil, fmt.Errorf("incorrect iframe invocation: %q", text) } return i, nil }