-
Notifications
You must be signed in to change notification settings - Fork 1k
Respect canonical import comments #1017
Changes from 6 commits
d6140bd
0e41a07
19c7f60
242e387
90d2a5e
1853dc3
47e42c7
611e64b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
) |
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" | ||
) |
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" | ||
) |
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" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,9 @@ | |
package pkgtree | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"go/ast" | ||
"go/build" | ||
"go/parser" | ||
gscan "go/scanner" | ||
|
@@ -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) | ||
|
||
|
@@ -152,18 +154,26 @@ func ListPackages(fileRoot, importRoot string) (PackageTree, error) { | |
} | ||
} | ||
|
||
if pkg.CommentPath != "" && !strings.HasPrefix(pkg.CommentPath, importRoot) { | ||
ptree.Packages[ip] = PackageOrErr{ | ||
Err: &NonCanonicalImportRoot{ | ||
ImportRoot: importRoot, | ||
Canonical: pkg.CommentPath, | ||
}, | ||
} | ||
return nil | ||
} | ||
|
||
// 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) | ||
} | ||
} | ||
|
@@ -210,6 +220,7 @@ func fillPackage(p *build.Package) error { | |
|
||
var testImports []string | ||
var imports []string | ||
var importComments []string | ||
for _, file := range gofiles { | ||
// Skip underscore-led or dot-led files, in keeping with the rest of the toolchain. | ||
bPrefix := filepath.Base(file)[0] | ||
|
@@ -234,6 +245,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 | ||
} | ||
|
@@ -282,14 +297,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 path. | ||
type ConflictingImportComments struct { | ||
ImportPath string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
ImportComments []string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For more clarity, let's name this |
||
} | ||
|
||
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 other than the package's declared canonical path. | ||
type NonCanonicalImportRoot struct { | ||
ImportRoot string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
(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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. microoptimization nit: |
||
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. | ||
// | ||
|
@@ -308,7 +407,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)) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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 😄