Skip to content
This repository has been archived by the owner on Sep 9, 2020. It is now read-only.

Respect canonical import comments #1017

Merged
merged 8 commits into from
Oct 7, 2017
Merged
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions internal/gps/_testdata/src/canon_confl/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.

package canonical // import "vanity1"

var (
A = "A"
)
9 changes: 9 additions & 0 deletions internal/gps/_testdata/src/canon_confl/b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.

package canonical // import "vanity2"

var (
B = "B"
)
9 changes: 9 additions & 0 deletions internal/gps/_testdata/src/canonical/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.

package pkg // import "canonical"

var (
A = "A"
)
5 changes: 5 additions & 0 deletions internal/gps/_testdata/src/canonical/sub/sub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 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.

package sub // import "canonical/subpackage"
4 changes: 2 additions & 2 deletions internal/gps/identifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ import (
type ProjectRoot string

// A ProjectIdentifier provides the name and source location of a dependency. It
// is related to, but differs in two keys ways from, an plain import path.
// is related to, but differs in two key ways from, a plain import path.
//
// First, ProjectIdentifiers do not identify a single package. Rather, they
// encompasses the whole tree of packages, including tree's root - the
// encompass the whole tree of packages, including tree's root - the
// ProjectRoot. In gps' current design, this ProjectRoot almost always
// corresponds to the root of a repository.
//
Expand Down
118 changes: 107 additions & 11 deletions internal/gps/pkgtree/pkgtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
package pkgtree

import (
"bytes"
"fmt"
"go/ast"
"go/build"
"go/parser"
gscan "go/scanner"
Expand Down Expand Up @@ -123,9 +125,9 @@ func ListPackages(fileRoot, importRoot string) (PackageTree, error) {
ip := filepath.ToSlash(filepath.Join(importRoot, strings.TrimPrefix(wp, fileRoot)))

// Find all the imports, across all os/arch combos
//p, err := fullPackageInDir(wp)
p := &build.Package{
Dir: wp,
Dir: wp,
ImportPath: ip,
}
err = fillPackage(p)

Expand All @@ -152,18 +154,23 @@ func ListPackages(fileRoot, importRoot string) (PackageTree, error) {
}
}

if pkg.CommentPath != "" && !strings.HasPrefix(pkg.CommentPath, importRoot) {
return &NonCanonicalImportRoot{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs softening a bit, as right now, the only cases where we return an actual error from ListPackages() itself (which is what this would result in) are more physical than logical - e.g., we can't actually read the filesystem. the rest of the API is built around that kind of assumption, too.

so, instead of returning the error directly, let's make this an error that we put in the PackageOrErr.Err slot on each package where we encounter the problem.

Copy link
Contributor Author

@rtfb rtfb Sep 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume an equivalent change should be made to prevent ConflictingImportComments from leaking as well?

ImportRoot: importRoot,
Canonical: pkg.CommentPath,
}
}

// This area has some...fuzzy rules, but check all the imports for
// local/relative/dot-ness, and record an error for the package if we
// see any.
var lim []string
for _, imp := range append(pkg.Imports, pkg.TestImports...) {
switch {
// Do allow the single-dot, at least for now
case imp == "..":
lim = append(lim, imp)
case strings.HasPrefix(imp, "./"):
lim = append(lim, imp)
case strings.HasPrefix(imp, "../"):
if build.IsLocalImport(imp) {
// Do allow the single-dot, at least for now
if imp == "." {
continue
}
lim = append(lim, imp)
}
}
Expand Down Expand Up @@ -210,6 +217,7 @@ func fillPackage(p *build.Package) error {

var testImports []string
var imports []string
var importComments []string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so i was originally going to suggest that we just keep a single string, rather than a []string, and upon encountering later values, just compare against that one string and error out immediately if we find an error. i was thinking that it might escape, but i ran it through with the optimization profiler (-gcflags '-m -m'), and turns out, it doesn't.

so, this is fine. just thought you might be interested that i checked that through 😄

for _, file := range gofiles {
// Skip underscore-led or dot-led files, in keeping with the rest of the toolchain.
bPrefix := filepath.Base(file)[0]
Expand All @@ -234,6 +242,10 @@ func fillPackage(p *build.Package) error {

var ignored bool
for _, c := range pf.Comments {
ic := findImportComment(pf.Name, c)
if ic != "" {
importComments = append(importComments, ic)
}
if c.Pos() > pf.Package { // +build comment must come before package
continue
}
Expand Down Expand Up @@ -282,14 +294,98 @@ func fillPackage(p *build.Package) error {
}
}
}

importComments = uniq(importComments)
if len(importComments) > 1 {
return &ConflictingImportComments{
ImportPath: p.ImportPath,
ImportComments: importComments,
}
}
if len(importComments) > 0 {
p.ImportComment = importComments[0]
}
imports = uniq(imports)
testImports = uniq(testImports)
p.Imports = imports
p.TestImports = testImports
return nil
}

var (
slashSlash = []byte("//")
slashStar = []byte("/*")
starSlash = []byte("*/")
newline = []byte("\n")
importKwd = []byte("import ")
)

func findImportComment(pkgName *ast.Ident, c *ast.CommentGroup) string {
afterPkg := pkgName.NamePos + token.Pos(len(pkgName.Name)) + 1
commentSlash := c.List[0].Slash
if afterPkg != commentSlash {
return ""
}
text := []byte(c.List[0].Text)
switch {
case bytes.HasPrefix(text, slashSlash):
eol := bytes.IndexByte(text, '\n')
if eol < 0 {
eol = len(text)
}
text = text[2:eol]
case bytes.HasPrefix(text, slashStar):
text = text[2:]
end := bytes.Index(text, starSlash)
if end < 0 {
// malformed comment
return ""
}
text = text[:end]
if bytes.IndexByte(text, '\n') > 0 {
// multiline comment, can't be an import comment
return ""
}
}
text = bytes.TrimSpace(text)
if !bytes.HasPrefix(text, importKwd) {
return ""
}
quotedPath := bytes.TrimSpace(text[len(importKwd):])
return string(bytes.Trim(quotedPath, `"`))
}

// ConflictingImportComments indicates that the package declares more than one
// different canonical paths.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/paths/path/

type ConflictingImportComments struct {
ImportPath string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the provenance of these errors can be a little confusing, let's include comments explaining the intent of the information recorded in each of the struct fields here, analogous to Package.

ImportComments []string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For more clarity, let's name this ConflictingImportComments.

}

func (e *ConflictingImportComments) Error() string {
return fmt.Sprintf("import path %s had conflicting import comments: %s",
e.ImportPath, quotedPaths(e.ImportComments))
}

// NonCanonicalImportRoot reports the situation when the dependee imports a
// package via something else that the package's declared canonical path.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/else that/other than/

type NonCanonicalImportRoot struct {
ImportRoot string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the above, let's include brief field-by-field comments on these to explain where they come from.

Canonical string
}

func (e *NonCanonicalImportRoot) Error() string {
return fmt.Sprintf("importing via path %q, but package insists on a canonical path %q",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this phrasing may end up being a bit confusing, as it would come out:

importing via path "github.com/foo/Bar", but package insists on a canonical path "github.com/foo/bar/bleep/blorp/bopkins"

(case variation here used only to provide a clear example, this probably isn't how it'll be seen most of the time)

i don't have a specific wording suggestion, but we should tweak this a little bit to accommodate the fact that we're contrasting a root path with what could potentially be a subpackage path.

e.ImportRoot, e.Canonical)
}

func quotedPaths(ps []string) string {
var quoted []string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

microoptimization nit: quoted := make([]string, 0, len(ps))

for _, p := range ps {
quoted = append(quoted, fmt.Sprintf("%q", p))
}
return strings.Join(quoted, ", ")
}

// LocalImportsError indicates that a package contains at least one relative
// import that will prevent it from compiling.
//
Expand All @@ -308,7 +404,7 @@ func (e *LocalImportsError) Error() string {
case 1:
return fmt.Sprintf("import path %s had a local import: %q", e.ImportPath, e.LocalImports[0])
default:
return fmt.Sprintf("import path %s had local imports: %q", e.ImportPath, strings.Join(e.LocalImports, "\", \""))
return fmt.Sprintf("import path %s had local imports: %s", e.ImportPath, quotedPaths(e.LocalImports))
}
}

Expand Down
46 changes: 46 additions & 0 deletions internal/gps/pkgtree/pkgtree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,52 @@ func TestListPackages(t *testing.T) {
},
},
},
"canonical": {
fileRoot: j("canonical"),
importRoot: "canonical",
out: PackageTree{
ImportRoot: "canonical",
Packages: map[string]PackageOrErr{
"canonical": {
P: Package{
ImportPath: "canonical",
CommentPath: "canonical",
Name: "pkg",
Imports: []string{},
},
},
"canonical/sub": {
P: Package{
ImportPath: "canonical/sub",
CommentPath: "canonical/subpackage",
Name: "sub",
Imports: []string{},
},
},
},
},
},
"conflicting canonical comments": {
fileRoot: j("canon_confl"),
importRoot: "canon_confl",
out: PackageTree{},
err: &ConflictingImportComments{
ImportPath: "canon_confl",
ImportComments: []string{
"vanity1",
"vanity2",
},
},
},
"non-canonical": {
fileRoot: j("canonical"),
importRoot: "noncanonical",
out: PackageTree{},
err: &NonCanonicalImportRoot{
ImportRoot: "noncanonical",
Canonical: "canonical",
},
},
}

for name, fix := range table {
Expand Down