Import the go-sumtype tool.
Use the tools build pattern to ensure that go-sumtype is always available for the build. Signed-off-by: James Peach <jpeach@cloudflare.com>
This commit is contained in:
parent
5431e0ca12
commit
33656a4c41
10
Makefile
10
Makefile
|
@ -3,6 +3,8 @@ MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut
|
||||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||||
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
||||||
|
|
||||||
|
GO_SUMTYPE := go run -mod=vendor github.com/sudarshan-reddy/go-sumtype
|
||||||
|
|
||||||
ifeq ($(ORIGINAL_NAME), true)
|
ifeq ($(ORIGINAL_NAME), true)
|
||||||
# Used for builds that want FIPS compilation but want the artifacts generated to still have the original name.
|
# Used for builds that want FIPS compilation but want the artifacts generated to still have the original name.
|
||||||
BINARY_NAME := cloudflared
|
BINARY_NAME := cloudflared
|
||||||
|
@ -271,12 +273,8 @@ quic-deps:
|
||||||
.PHONY: vet
|
.PHONY: vet
|
||||||
vet:
|
vet:
|
||||||
go vet -mod=vendor ./...
|
go vet -mod=vendor ./...
|
||||||
# go get github.com/sudarshan-reddy/go-sumtype (don't do this in build directory or this will cause vendor issues)
|
$(GO_SUMTYPE) $$(go list -mod=vendor ./...)
|
||||||
# Note: If you have github.com/BurntSushi/go-sumtype then you might have to use the repo above instead
|
|
||||||
# for now because it uses an older version of golang.org/x/tools.
|
|
||||||
which go-sumtype
|
|
||||||
go-sumtype $$(go list -mod=vendor ./...)
|
|
||||||
|
|
||||||
.PHONY: goimports
|
.PHONY: goimports
|
||||||
goimports:
|
goimports:
|
||||||
for d in $$(go list -mod=readonly -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -w $$d ; done
|
for d in $$(go list -mod=readonly -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -w $$d ; done
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -41,6 +41,7 @@ require (
|
||||||
github.com/rs/zerolog v1.20.0
|
github.com/rs/zerolog v1.20.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
|
github.com/sudarshan-reddy/go-sumtype v0.0.0-20210827105221-82eca7e5abb1
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
go.uber.org/automaxprocs v1.4.0
|
go.uber.org/automaxprocs v1.4.0
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -562,6 +562,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/sudarshan-reddy/go-sumtype v0.0.0-20210827105221-82eca7e5abb1 h1:PfYPROl/6Zyspmiebusz0s/Bsr/Ju/U9TFwkPqzdL3U=
|
||||||
|
github.com/sudarshan-reddy/go-sumtype v0.0.0-20210827105221-82eca7e5abb1/go.mod h1:g+9pWOY/CQta4Y1Z7oTU9BcBGs2awNSLJ4aBRyvG784=
|
||||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||||
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
||||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
//go:build tools
|
||||||
|
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/sudarshan-reddy/go-sumtype"
|
||||||
|
)
|
|
@ -0,0 +1 @@
|
||||||
|
language: go
|
|
@ -0,0 +1,3 @@
|
||||||
|
This project is dual-licensed under the Unlicense and MIT licenses.
|
||||||
|
|
||||||
|
You may use this code under the terms of either license.
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Andrew Gallant
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,124 @@
|
||||||
|
go-sumtype
|
||||||
|
==========
|
||||||
|
A simple utility for running exhaustiveness checks on type switch statements.
|
||||||
|
Exhaustiveness checks are only run on interfaces that are declared to be
|
||||||
|
"sum types."
|
||||||
|
|
||||||
|
[![Linux build status](https://api.travis-ci.org/BurntSushi/go-sumtype.png)](https://travis-ci.org/BurntSushi/go-sumtype)
|
||||||
|
|
||||||
|
Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org).
|
||||||
|
|
||||||
|
This work was inspired by our code at
|
||||||
|
[Diffeo](https://diffeo.com).
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```go
|
||||||
|
$ go get github.com/BurntSushi/go-sumtype
|
||||||
|
```
|
||||||
|
|
||||||
|
For usage info, just run the command:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go-sumtype
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical usage might look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go-sumtype $(go list ./... | grep -v vendor)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
go-sumtype takes a list of Go package paths or files and looks for sum type
|
||||||
|
declarations in each package/file provided. Exhaustiveness checks are then
|
||||||
|
performed for each use of a declared sum type in a type switch statement.
|
||||||
|
Namely, `go-sumtype` will report an error for any type switch statement that
|
||||||
|
either lacks a `default` clause or does not account for all possible variants.
|
||||||
|
|
||||||
|
Declarations are provided in comments like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
//go-sumtype:decl MySumType
|
||||||
|
```
|
||||||
|
|
||||||
|
`MySumType` must satisfy the following:
|
||||||
|
|
||||||
|
1. It is a type defined in the same package.
|
||||||
|
2. It is an interface.
|
||||||
|
3. It is *sealed*. That is, part of its interface definition contains an
|
||||||
|
unexported method.
|
||||||
|
|
||||||
|
`go-sumtype` will produce an error if any of the above is not true.
|
||||||
|
|
||||||
|
For valid declarations, `go-sumtype` will look for all occurrences in which a
|
||||||
|
value of type `MySumType` participates in a type switch statement. In those
|
||||||
|
occurrences, it will attempt to detect whether the type switch is exhaustive
|
||||||
|
or not. If it's not, `go-sumtype` will report an error. For example, running
|
||||||
|
`go-sumtype` on this source file:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go-sumtype:decl MySumType
|
||||||
|
|
||||||
|
type MySumType interface {
|
||||||
|
sealed()
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariantA struct{}
|
||||||
|
|
||||||
|
func (*VariantA) sealed() {}
|
||||||
|
|
||||||
|
type VariantB struct{}
|
||||||
|
|
||||||
|
func (*VariantB) sealed() {}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
switch MySumType(nil).(type) {
|
||||||
|
case *VariantA:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
produces the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go-sumtype mysumtype.go
|
||||||
|
mysumtype.go:18:2: exhaustiveness check failed for sum type 'MySumType': missing cases for VariantB
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding either a `default` clause or a clause to handle `*VariantB` will cause
|
||||||
|
exhaustive checks to pass.
|
||||||
|
|
||||||
|
As a special case, if the type switch statement contains a `default` clause
|
||||||
|
that always panics, then exhaustiveness checks are still performed.
|
||||||
|
|
||||||
|
### Details and motivation
|
||||||
|
|
||||||
|
Sum types are otherwise known as discriminated unions. That is, a sum type is
|
||||||
|
a finite set of disjoint values. In type systems that support sum types, the
|
||||||
|
language will guarantee that if one has a sum type `T`, then its value must
|
||||||
|
be one of its variants.
|
||||||
|
|
||||||
|
Go's type system does not support sum types. A typical proxy for representing
|
||||||
|
sum types in Go is to use an interface with an unexported method and define
|
||||||
|
each variant of the sum type in the same package to satisfy said interface.
|
||||||
|
This guarantees that the set of types that satisfy the interface is closed
|
||||||
|
at compile time. Performing case analysis on these types is then done with
|
||||||
|
a type switch statement, e.g., `switch x.(type) { ... }`. Each clause of the
|
||||||
|
type switch corresponds to a *variant* of the sum type. The downside of this
|
||||||
|
approach is that Go's type system is not aware of the set of variants, so it
|
||||||
|
cannot tell you whether case analysis over a sum type is complete or not.
|
||||||
|
|
||||||
|
The `go-sumtype` command recognizes this pattern, but it needs a small amount
|
||||||
|
of help to recognize which interfaces should be treated as sum types, which
|
||||||
|
is why the `//go-sumtype:decl ...` annotation is required. `go-sumtype` will
|
||||||
|
figure out all of the variants of a sum type by finding the set of types
|
||||||
|
defined in the same package that satisfy the interface specified by the
|
||||||
|
declaration.
|
||||||
|
|
||||||
|
The `go-sumtype` command will prove its worth when you need to add a variant
|
||||||
|
to an existing sum type. Running `go-sumtype` will tell you immediately which
|
||||||
|
case analyses need to be updated to account for the new variant.
|
|
@ -0,0 +1,24 @@
|
||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org/>
|
|
@ -0,0 +1,183 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// inexhaustiveError is returned from check for each occurrence of inexhaustive
|
||||||
|
// case analysis in a Go type switch statement.
|
||||||
|
type inexhaustiveError struct {
|
||||||
|
Pos token.Position
|
||||||
|
Def sumTypeDef
|
||||||
|
Missing []types.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e inexhaustiveError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s: exhaustiveness check failed for sum type '%s': missing cases for %s",
|
||||||
|
e.Pos, e.Def.Decl.TypeName, strings.Join(e.Names(), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names returns a sorted list of names corresponding to the missing variant
|
||||||
|
// cases.
|
||||||
|
func (e inexhaustiveError) Names() []string {
|
||||||
|
var list []string
|
||||||
|
for _, o := range e.Missing {
|
||||||
|
list = append(list, o.Name())
|
||||||
|
}
|
||||||
|
sort.Sort(sort.StringSlice(list))
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// check does exhaustiveness checking for the given sum type definitions in the
|
||||||
|
// given package. Every instance of inexhaustive case analysis is returned.
|
||||||
|
func check(pkg *packages.Package, defs []sumTypeDef) []error {
|
||||||
|
var errs []error
|
||||||
|
for _, astfile := range pkg.Syntax {
|
||||||
|
ast.Inspect(astfile, func(n ast.Node) bool {
|
||||||
|
swtch, ok := n.(*ast.TypeSwitchStmt)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err := checkSwitch(pkg, defs, swtch); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSwitch performs an exhaustiveness check on the given type switch
|
||||||
|
// statement. If the type switch is used on a sum type and does not cover
|
||||||
|
// all variants of that sum type, then an error is returned indicating which
|
||||||
|
// variants were missed.
|
||||||
|
//
|
||||||
|
// Note that if the type switch contains a non-panicing default case, then
|
||||||
|
// exhaustiveness checks are disabled.
|
||||||
|
func checkSwitch(
|
||||||
|
pkg *packages.Package,
|
||||||
|
defs []sumTypeDef,
|
||||||
|
swtch *ast.TypeSwitchStmt,
|
||||||
|
) error {
|
||||||
|
def, missing := missingVariantsInSwitch(pkg, defs, swtch)
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return inexhaustiveError{
|
||||||
|
Pos: pkg.Fset.Position(swtch.Pos()),
|
||||||
|
Def: *def,
|
||||||
|
Missing: missing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// missingVariantsInSwitch returns a list of missing variants corresponding to
|
||||||
|
// the given switch statement. The corresponding sum type definition is also
|
||||||
|
// returned. (If no sum type definition could be found, then no exhaustiveness
|
||||||
|
// checks are performed, and therefore, no missing variants are returned.)
|
||||||
|
func missingVariantsInSwitch(
|
||||||
|
pkg *packages.Package,
|
||||||
|
defs []sumTypeDef,
|
||||||
|
swtch *ast.TypeSwitchStmt,
|
||||||
|
) (*sumTypeDef, []types.Object) {
|
||||||
|
asserted := findTypeAssertExpr(swtch)
|
||||||
|
ty := pkg.TypesInfo.TypeOf(asserted)
|
||||||
|
def := findDef(defs, ty)
|
||||||
|
if def == nil {
|
||||||
|
// We couldn't find a corresponding sum type, so there's
|
||||||
|
// nothing we can do to check it.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
variantExprs, hasDefault := switchVariants(swtch)
|
||||||
|
if hasDefault && !defaultClauseAlwaysPanics(swtch) {
|
||||||
|
// A catch-all case defeats all exhaustiveness checks.
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
var variantTypes []types.Type
|
||||||
|
for _, expr := range variantExprs {
|
||||||
|
variantTypes = append(variantTypes, pkg.TypesInfo.TypeOf(expr))
|
||||||
|
}
|
||||||
|
return def, def.missing(variantTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// switchVariants returns all case expressions found in a type switch. This
|
||||||
|
// includes expressions from cases that have a list of expressions.
|
||||||
|
func switchVariants(swtch *ast.TypeSwitchStmt) (exprs []ast.Expr, hasDefault bool) {
|
||||||
|
for _, stmt := range swtch.Body.List {
|
||||||
|
clause := stmt.(*ast.CaseClause)
|
||||||
|
if clause.List == nil {
|
||||||
|
hasDefault = true
|
||||||
|
} else {
|
||||||
|
exprs = append(exprs, clause.List...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultClauseAlwaysPanics returns true if the given switch statement has a
|
||||||
|
// default clause that always panics. Note that this is done on a best-effort
|
||||||
|
// basis. While there will never be any false positives, there may be false
|
||||||
|
// negatives.
|
||||||
|
//
|
||||||
|
// If the given switch statement has no default clause, then this function
|
||||||
|
// panics.
|
||||||
|
func defaultClauseAlwaysPanics(swtch *ast.TypeSwitchStmt) bool {
|
||||||
|
var clause *ast.CaseClause
|
||||||
|
for _, stmt := range swtch.Body.List {
|
||||||
|
c := stmt.(*ast.CaseClause)
|
||||||
|
if c.List == nil {
|
||||||
|
clause = c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clause == nil {
|
||||||
|
panic("switch statement has no default clause")
|
||||||
|
}
|
||||||
|
if len(clause.Body) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
exprStmt, ok := clause.Body[0].(*ast.ExprStmt)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
callExpr, ok := exprStmt.X.(*ast.CallExpr)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fun, ok := callExpr.Fun.(*ast.Ident)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fun.Name == "panic"
|
||||||
|
}
|
||||||
|
|
||||||
|
// findTypeAssertExpr extracts the expression that is being type asserted from a
|
||||||
|
// type swtich statement.
|
||||||
|
func findTypeAssertExpr(swtch *ast.TypeSwitchStmt) ast.Expr {
|
||||||
|
var expr ast.Expr
|
||||||
|
if assign, ok := swtch.Assign.(*ast.AssignStmt); ok {
|
||||||
|
expr = assign.Rhs[0]
|
||||||
|
} else {
|
||||||
|
expr = swtch.Assign.(*ast.ExprStmt).X
|
||||||
|
}
|
||||||
|
return expr.(*ast.TypeAssertExpr).X
|
||||||
|
}
|
||||||
|
|
||||||
|
// findDef returns the sum type definition corresponding to the given type. If
|
||||||
|
// no such sum type definition exists, then nil is returned.
|
||||||
|
func findDef(defs []sumTypeDef, needle types.Type) *sumTypeDef {
|
||||||
|
for i := range defs {
|
||||||
|
def := &defs[i]
|
||||||
|
if types.Identical(needle.Underlying(), def.Ty) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sumTypeDecl is a declaration of a sum type in a Go source file.
|
||||||
|
type sumTypeDecl struct {
|
||||||
|
// The package path that contains this decl.
|
||||||
|
Package *packages.Package
|
||||||
|
// The type named by this decl.
|
||||||
|
TypeName string
|
||||||
|
// The file path where this declaration was found.
|
||||||
|
Path string
|
||||||
|
// The line number where this declaration was found.
|
||||||
|
Line int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns a short string describing where this declaration was found.
|
||||||
|
func (d sumTypeDecl) Location() string {
|
||||||
|
return fmt.Sprintf("%s:%d", d.Path, d.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSumTypeDecls searches every package given for sum type declarations of
|
||||||
|
// the form `go-sumtype:decl ...`.
|
||||||
|
func findSumTypeDecls(pkgs []*packages.Package) ([]sumTypeDecl, error) {
|
||||||
|
var decls []sumTypeDecl
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
for _, filename := range pkg.CompiledGoFiles {
|
||||||
|
if filepath.Base(filename) == "C" {
|
||||||
|
// ignore (fake?) cgo files
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileDecls, err := sumTypeDeclSearch(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range fileDecls {
|
||||||
|
fileDecls[i].Package = pkg
|
||||||
|
}
|
||||||
|
decls = append(decls, fileDecls...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sumTypeDeclSearch searches the given file for sum type declarations of the
|
||||||
|
// form `go-sumtype:decl ...`.
|
||||||
|
func sumTypeDeclSearch(path string) ([]sumTypeDecl, error) {
|
||||||
|
var decls []sumTypeDecl
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
lineNum := 0
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lineNum++
|
||||||
|
line := scanner.Bytes()
|
||||||
|
if !isSumTypeDecl(line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ty := parseSumTypeDecl(line)
|
||||||
|
if len(ty) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decls = append(decls, sumTypeDecl{
|
||||||
|
TypeName: ty,
|
||||||
|
Path: path,
|
||||||
|
Line: lineNum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
// A scanner can puke if it hits a line that is too long.
|
||||||
|
// We assume such files won't contain any future decls and
|
||||||
|
// otherwise move on.
|
||||||
|
log.Printf("scan error reading '%s': %s", path, err)
|
||||||
|
}
|
||||||
|
return decls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var reParseSumTypeDecl = regexp.MustCompile(`^//go-sumtype:decl\s+(\S+)\s*$`)
|
||||||
|
|
||||||
|
// parseSumTypeDecl parses the type name out of a sum type decl.
|
||||||
|
//
|
||||||
|
// If no such decl could be found, then this returns an empty string.
|
||||||
|
func parseSumTypeDecl(line []byte) string {
|
||||||
|
caps := reParseSumTypeDecl.FindSubmatch(line)
|
||||||
|
if len(caps) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(caps[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSumTypeDecl returns true if and only if this line in a Go source file
|
||||||
|
// is a sum type decl.
|
||||||
|
func isSumTypeDecl(line []byte) bool {
|
||||||
|
variant1, variant2 := []byte("//go-sumtype:decl "), []byte("//go-sumtype:decl\t")
|
||||||
|
return bytes.HasPrefix(line, variant1) || bytes.HasPrefix(line, variant2)
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unsealedError corresponds to a declared sum type whose interface is not
|
||||||
|
// sealed. A sealed interface requires at least one unexported method.
|
||||||
|
type unsealedError struct {
|
||||||
|
Decl sumTypeDecl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e unsealedError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s: interface '%s' is not sealed "+
|
||||||
|
"(sealing requires at least one unexported method)",
|
||||||
|
e.Decl.Location(), e.Decl.TypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notFoundError corresponds to a declared sum type whose type definition
|
||||||
|
// could not be found in the same Go package.
|
||||||
|
type notFoundError struct {
|
||||||
|
Decl sumTypeDecl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e notFoundError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: type '%s' is not defined", e.Decl.Location(), e.Decl.TypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notInterfaceError corresponds to a declared sum type that does not
|
||||||
|
// correspond to an interface.
|
||||||
|
type notInterfaceError struct {
|
||||||
|
Decl sumTypeDecl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e notInterfaceError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: type '%s' is not an interface", e.Decl.Location(), e.Decl.TypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sumTypeDef corresponds to the definition of a Go interface that is
|
||||||
|
// interpreted as a sum type. Its variants are determined by finding all types
|
||||||
|
// that implement said interface in the same package.
|
||||||
|
type sumTypeDef struct {
|
||||||
|
Decl sumTypeDecl
|
||||||
|
Ty *types.Interface
|
||||||
|
Variants []types.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSumTypeDefs attempts to find a Go type definition for each of the given
|
||||||
|
// sum type declarations. If no such sum type definition could be found for
|
||||||
|
// any of the given declarations, then an error is returned.
|
||||||
|
func findSumTypeDefs(decls []sumTypeDecl) ([]sumTypeDef, []error) {
|
||||||
|
var defs []sumTypeDef
|
||||||
|
var errs []error
|
||||||
|
for _, decl := range decls {
|
||||||
|
def, err := newSumTypeDef(decl.Package.Types, decl)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if def == nil {
|
||||||
|
errs = append(errs, notFoundError{decl})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defs = append(defs, *def)
|
||||||
|
}
|
||||||
|
return defs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSumTypeDef attempts to extract a sum type definition from a single
|
||||||
|
// package. If no such type corresponds to the given decl, then this function
|
||||||
|
// returns a nil def and a nil error.
|
||||||
|
//
|
||||||
|
// If the decl corresponds to a type that isn't an interface containing at
|
||||||
|
// least one unexported method, then this returns an error.
|
||||||
|
func newSumTypeDef(pkg *types.Package, decl sumTypeDecl) (*sumTypeDef, error) {
|
||||||
|
obj := pkg.Scope().Lookup(decl.TypeName)
|
||||||
|
if obj == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
iface, ok := obj.Type().Underlying().(*types.Interface)
|
||||||
|
if !ok {
|
||||||
|
return nil, notInterfaceError{decl}
|
||||||
|
}
|
||||||
|
hasUnexported := false
|
||||||
|
for i := 0; i < iface.NumMethods(); i++ {
|
||||||
|
if !iface.Method(i).Exported() {
|
||||||
|
hasUnexported = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasUnexported {
|
||||||
|
return nil, unsealedError{decl}
|
||||||
|
}
|
||||||
|
def := &sumTypeDef{
|
||||||
|
Decl: decl,
|
||||||
|
Ty: iface,
|
||||||
|
}
|
||||||
|
for _, name := range pkg.Scope().Names() {
|
||||||
|
obj, ok := pkg.Scope().Lookup(name).(*types.TypeName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ty := obj.Type()
|
||||||
|
if types.Identical(ty.Underlying(), iface) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if types.Implements(ty, iface) || types.Implements(types.NewPointer(ty), iface) {
|
||||||
|
def.Variants = append(def.Variants, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (def *sumTypeDef) String() string {
|
||||||
|
return def.Decl.TypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing returns a list of variants in this sum type that are not in the
|
||||||
|
// given list of types.
|
||||||
|
func (def *sumTypeDef) missing(tys []types.Type) []types.Object {
|
||||||
|
// TODO(ag): This is O(n^2). Fix that. /shrug
|
||||||
|
var missing []types.Object
|
||||||
|
for _, v := range def.Variants {
|
||||||
|
found := false
|
||||||
|
varty := indirect(v.Type())
|
||||||
|
for _, ty := range tys {
|
||||||
|
ty = indirect(ty)
|
||||||
|
if types.Identical(varty, ty) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
missing = append(missing, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing
|
||||||
|
}
|
||||||
|
|
||||||
|
// indirect dereferences through an arbitrary number of pointer types.
|
||||||
|
func indirect(ty types.Type) types.Type {
|
||||||
|
if ty, ok := ty.(*types.Pointer); ok {
|
||||||
|
return indirect(ty.Elem())
|
||||||
|
}
|
||||||
|
return ty
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkgFiles(pkg *ast.Package) []*ast.File {
|
||||||
|
var files []*ast.File
|
||||||
|
for _, file := range pkg.Files {
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
go-sumtype takes a list of Go package paths or files and looks for sum type
|
||||||
|
declarations in each package/file provided. Exhaustiveness checks are then
|
||||||
|
performed for each use of a declared sum type in a type switch statement.
|
||||||
|
Namely, go-sumtype will report an error for any type switch statement that
|
||||||
|
either lacks a default clause or does not account for all possible variants.
|
||||||
|
|
||||||
|
Declarations are provided in comments like so:
|
||||||
|
|
||||||
|
//go-sumtype:decl MySumType
|
||||||
|
|
||||||
|
MySumType must satisfy the following:
|
||||||
|
|
||||||
|
1. It is a type defined in the same package.
|
||||||
|
2. It is an interface.
|
||||||
|
3. It is *sealed*. That is, part of its interface definition contains an
|
||||||
|
unexported method.
|
||||||
|
|
||||||
|
go-sumtype will produce an error if any of the above is not true.
|
||||||
|
|
||||||
|
For valid declarations, go-sumtype will look for all occurrences in which a
|
||||||
|
value of type MySumType participates in a type switch statement. In those
|
||||||
|
occurrences, it will attempt to detect whether the type switch is exhaustive
|
||||||
|
or not. If it's not, go-sumtype will report an error. For example:
|
||||||
|
|
||||||
|
$ cat mysumtype.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
//go-sumtype:decl MySumType
|
||||||
|
|
||||||
|
type MySumType interface {
|
||||||
|
sealed()
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariantA struct{}
|
||||||
|
|
||||||
|
func (a *VariantA) sealed() {}
|
||||||
|
|
||||||
|
type VariantB struct{}
|
||||||
|
|
||||||
|
func (b *VariantB) sealed() {}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
switch MySumType(nil).(type) {
|
||||||
|
case *VariantA:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ go-sumtype mysumtype.go
|
||||||
|
mysumtype.go:18:2: exhaustiveness check failed for sum type 'MySumType': missing cases for VariantB
|
||||||
|
|
||||||
|
Adding either a default clause or a clause to handle *VariantB will cause
|
||||||
|
exhaustive checks to pass.
|
||||||
|
|
||||||
|
As a special case, if the type switch statement contains a default clause
|
||||||
|
that always panics, then exhaustiveness checks are still performed.
|
||||||
|
*/
|
||||||
|
package main
|
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/loader"
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
// TODO: Switch this to use golang.org/x/tools/go/packages.
|
||||||
|
log.Fatalf("Usage: go-sumtype <args>\n%s", loader.FromArgsUsage)
|
||||||
|
}
|
||||||
|
args := os.Args[1:]
|
||||||
|
pkgs, err := tycheckAll(args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if errs := run(pkgs); len(errs) > 0 {
|
||||||
|
var list []string
|
||||||
|
for _, err := range errs {
|
||||||
|
list = append(list, err.Error())
|
||||||
|
}
|
||||||
|
log.Fatal(strings.Join(list, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(pkgs []*packages.Package) []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
decls, err := findSumTypeDecls(pkgs)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
defs, defErrs := findSumTypeDefs(decls)
|
||||||
|
errs = append(errs, defErrs...)
|
||||||
|
if len(defs) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
if pkgErrs := check(pkg, defs); pkgErrs != nil {
|
||||||
|
errs = append(errs, pkgErrs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func tycheckAll(args []string) ([]*packages.Package, error) {
|
||||||
|
conf := &packages.Config{
|
||||||
|
Mode: packages.LoadSyntax,
|
||||||
|
// Unfortunately, it appears including the test packages in
|
||||||
|
// this lint makes it difficult to do exhaustiveness checking.
|
||||||
|
// Namely, it appears that compiling the test version of a
|
||||||
|
// package introduces distinct types from the normal version
|
||||||
|
// of the package, which will always result in inexhaustive
|
||||||
|
// errors whenever a package both defines a sum type and has
|
||||||
|
// tests. (Specifically, using `package name`. Using `package
|
||||||
|
// name_test` is OK.)
|
||||||
|
//
|
||||||
|
// It's not clear what the best way to fix this is. :-(
|
||||||
|
Tests: false,
|
||||||
|
}
|
||||||
|
pkgs, err := packages.Load(conf, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkgs, nil
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
// Copyright 2014 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 buildutil provides utilities related to the go/build
|
||||||
|
// package in the standard library.
|
||||||
|
//
|
||||||
|
// All I/O is done via the build.Context file system interface, which must
|
||||||
|
// be concurrency-safe.
|
||||||
|
package buildutil // import "golang.org/x/tools/go/buildutil"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/build"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllPackages returns the package path of each Go package in any source
|
||||||
|
// directory of the specified build context (e.g. $GOROOT or an element
|
||||||
|
// of $GOPATH). Errors are ignored. The results are sorted.
|
||||||
|
// All package paths are canonical, and thus may contain "/vendor/".
|
||||||
|
//
|
||||||
|
// The result may include import paths for directories that contain no
|
||||||
|
// *.go files, such as "archive" (in $GOROOT/src).
|
||||||
|
//
|
||||||
|
// All I/O is done via the build.Context file system interface,
|
||||||
|
// which must be concurrency-safe.
|
||||||
|
//
|
||||||
|
func AllPackages(ctxt *build.Context) []string {
|
||||||
|
var list []string
|
||||||
|
ForEachPackage(ctxt, func(pkg string, _ error) {
|
||||||
|
list = append(list, pkg)
|
||||||
|
})
|
||||||
|
sort.Strings(list)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEachPackage calls the found function with the package path of
|
||||||
|
// each Go package it finds in any source directory of the specified
|
||||||
|
// build context (e.g. $GOROOT or an element of $GOPATH).
|
||||||
|
// All package paths are canonical, and thus may contain "/vendor/".
|
||||||
|
//
|
||||||
|
// If the package directory exists but could not be read, the second
|
||||||
|
// argument to the found function provides the error.
|
||||||
|
//
|
||||||
|
// All I/O is done via the build.Context file system interface,
|
||||||
|
// which must be concurrency-safe.
|
||||||
|
//
|
||||||
|
func ForEachPackage(ctxt *build.Context, found func(importPath string, err error)) {
|
||||||
|
ch := make(chan item)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, root := range ctxt.SrcDirs() {
|
||||||
|
root := root
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
allPackages(ctxt, root, ch)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// All calls to found occur in the caller's goroutine.
|
||||||
|
for i := range ch {
|
||||||
|
found(i.importPath, i.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
importPath string
|
||||||
|
err error // (optional)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use a process-wide counting semaphore to limit
|
||||||
|
// the number of parallel calls to ReadDir.
|
||||||
|
var ioLimit = make(chan bool, 20)
|
||||||
|
|
||||||
|
func allPackages(ctxt *build.Context, root string, ch chan<- item) {
|
||||||
|
root = filepath.Clean(root) + string(os.PathSeparator)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
var walkDir func(dir string)
|
||||||
|
walkDir = func(dir string) {
|
||||||
|
// Avoid .foo, _foo, and testdata directory trees.
|
||||||
|
base := filepath.Base(dir)
|
||||||
|
if base == "" || base[0] == '.' || base[0] == '_' || base == "testdata" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg := filepath.ToSlash(strings.TrimPrefix(dir, root))
|
||||||
|
|
||||||
|
// Prune search if we encounter any of these import paths.
|
||||||
|
switch pkg {
|
||||||
|
case "builtin":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ioLimit <- true
|
||||||
|
files, err := ReadDir(ctxt, dir)
|
||||||
|
<-ioLimit
|
||||||
|
if pkg != "" || err != nil {
|
||||||
|
ch <- item{pkg, err}
|
||||||
|
}
|
||||||
|
for _, fi := range files {
|
||||||
|
fi := fi
|
||||||
|
if fi.IsDir() {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
walkDir(filepath.Join(dir, fi.Name()))
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walkDir(root)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandPatterns returns the set of packages matched by patterns,
|
||||||
|
// which may have the following forms:
|
||||||
|
//
|
||||||
|
// golang.org/x/tools/cmd/guru # a single package
|
||||||
|
// golang.org/x/tools/... # all packages beneath dir
|
||||||
|
// ... # the entire workspace.
|
||||||
|
//
|
||||||
|
// Order is significant: a pattern preceded by '-' removes matching
|
||||||
|
// packages from the set. For example, these patterns match all encoding
|
||||||
|
// packages except encoding/xml:
|
||||||
|
//
|
||||||
|
// encoding/... -encoding/xml
|
||||||
|
//
|
||||||
|
// A trailing slash in a pattern is ignored. (Path components of Go
|
||||||
|
// package names are separated by slash, not the platform's path separator.)
|
||||||
|
//
|
||||||
|
func ExpandPatterns(ctxt *build.Context, patterns []string) map[string]bool {
|
||||||
|
// TODO(adonovan): support other features of 'go list':
|
||||||
|
// - "std"/"cmd"/"all" meta-packages
|
||||||
|
// - "..." not at the end of a pattern
|
||||||
|
// - relative patterns using "./" or "../" prefix
|
||||||
|
|
||||||
|
pkgs := make(map[string]bool)
|
||||||
|
doPkg := func(pkg string, neg bool) {
|
||||||
|
if neg {
|
||||||
|
delete(pkgs, pkg)
|
||||||
|
} else {
|
||||||
|
pkgs[pkg] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan entire workspace if wildcards are present.
|
||||||
|
// TODO(adonovan): opt: scan only the necessary subtrees of the workspace.
|
||||||
|
var all []string
|
||||||
|
for _, arg := range patterns {
|
||||||
|
if strings.HasSuffix(arg, "...") {
|
||||||
|
all = AllPackages(ctxt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range patterns {
|
||||||
|
if arg == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
neg := arg[0] == '-'
|
||||||
|
if neg {
|
||||||
|
arg = arg[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg == "..." {
|
||||||
|
// ... matches all packages
|
||||||
|
for _, pkg := range all {
|
||||||
|
doPkg(pkg, neg)
|
||||||
|
}
|
||||||
|
} else if dir := strings.TrimSuffix(arg, "/..."); dir != arg {
|
||||||
|
// dir/... matches all packages beneath dir
|
||||||
|
for _, pkg := range all {
|
||||||
|
if strings.HasPrefix(pkg, dir) &&
|
||||||
|
(len(pkg) == len(dir) || pkg[len(dir)] == '/') {
|
||||||
|
doPkg(pkg, neg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// single package
|
||||||
|
doPkg(strings.TrimSuffix(arg, "/"), neg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkgs
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2015 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 buildutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FakeContext returns a build.Context for the fake file tree specified
|
||||||
|
// by pkgs, which maps package import paths to a mapping from file base
|
||||||
|
// names to contents.
|
||||||
|
//
|
||||||
|
// The fake Context has a GOROOT of "/go" and no GOPATH, and overrides
|
||||||
|
// the necessary file access methods to read from memory instead of the
|
||||||
|
// real file system.
|
||||||
|
//
|
||||||
|
// Unlike a real file tree, the fake one has only two levels---packages
|
||||||
|
// and files---so ReadDir("/go/src/") returns all packages under
|
||||||
|
// /go/src/ including, for instance, "math" and "math/big".
|
||||||
|
// ReadDir("/go/src/math/big") would return all the files in the
|
||||||
|
// "math/big" package.
|
||||||
|
//
|
||||||
|
func FakeContext(pkgs map[string]map[string]string) *build.Context {
|
||||||
|
clean := func(filename string) string {
|
||||||
|
f := path.Clean(filepath.ToSlash(filename))
|
||||||
|
// Removing "/go/src" while respecting segment
|
||||||
|
// boundaries has this unfortunate corner case:
|
||||||
|
if f == "/go/src" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimPrefix(f, "/go/src/")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt := build.Default // copy
|
||||||
|
ctxt.GOROOT = "/go"
|
||||||
|
ctxt.GOPATH = ""
|
||||||
|
ctxt.Compiler = "gc"
|
||||||
|
ctxt.IsDir = func(dir string) bool {
|
||||||
|
dir = clean(dir)
|
||||||
|
if dir == "" {
|
||||||
|
return true // needed by (*build.Context).SrcDirs
|
||||||
|
}
|
||||||
|
return pkgs[dir] != nil
|
||||||
|
}
|
||||||
|
ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
|
||||||
|
dir = clean(dir)
|
||||||
|
var fis []os.FileInfo
|
||||||
|
if dir == "" {
|
||||||
|
// enumerate packages
|
||||||
|
for importPath := range pkgs {
|
||||||
|
fis = append(fis, fakeDirInfo(importPath))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// enumerate files of package
|
||||||
|
for basename := range pkgs[dir] {
|
||||||
|
fis = append(fis, fakeFileInfo(basename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(byName(fis))
|
||||||
|
return fis, nil
|
||||||
|
}
|
||||||
|
ctxt.OpenFile = func(filename string) (io.ReadCloser, error) {
|
||||||
|
filename = clean(filename)
|
||||||
|
dir, base := path.Split(filename)
|
||||||
|
content, ok := pkgs[path.Clean(dir)][base]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("file not found: %s", filename)
|
||||||
|
}
|
||||||
|
return ioutil.NopCloser(strings.NewReader(content)), nil
|
||||||
|
}
|
||||||
|
ctxt.IsAbsPath = func(path string) bool {
|
||||||
|
path = filepath.ToSlash(path)
|
||||||
|
// Don't rely on the default (filepath.Path) since on
|
||||||
|
// Windows, it reports virtual paths as non-absolute.
|
||||||
|
return strings.HasPrefix(path, "/")
|
||||||
|
}
|
||||||
|
return &ctxt
|
||||||
|
}
|
||||||
|
|
||||||
|
type byName []os.FileInfo
|
||||||
|
|
||||||
|
func (s byName) Len() int { return len(s) }
|
||||||
|
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
|
||||||
|
|
||||||
|
type fakeFileInfo string
|
||||||
|
|
||||||
|
func (fi fakeFileInfo) Name() string { return string(fi) }
|
||||||
|
func (fakeFileInfo) Sys() interface{} { return nil }
|
||||||
|
func (fakeFileInfo) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (fakeFileInfo) IsDir() bool { return false }
|
||||||
|
func (fakeFileInfo) Size() int64 { return 0 }
|
||||||
|
func (fakeFileInfo) Mode() os.FileMode { return 0644 }
|
||||||
|
|
||||||
|
type fakeDirInfo string
|
||||||
|
|
||||||
|
func (fd fakeDirInfo) Name() string { return string(fd) }
|
||||||
|
func (fakeDirInfo) Sys() interface{} { return nil }
|
||||||
|
func (fakeDirInfo) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (fakeDirInfo) IsDir() bool { return true }
|
||||||
|
func (fakeDirInfo) Size() int64 { return 0 }
|
||||||
|
func (fakeDirInfo) Mode() os.FileMode { return 0755 }
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package buildutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OverlayContext overlays a build.Context with additional files from
|
||||||
|
// a map. Files in the map take precedence over other files.
|
||||||
|
//
|
||||||
|
// In addition to plain string comparison, two file names are
|
||||||
|
// considered equal if their base names match and their directory
|
||||||
|
// components point at the same directory on the file system. That is,
|
||||||
|
// symbolic links are followed for directories, but not files.
|
||||||
|
//
|
||||||
|
// A common use case for OverlayContext is to allow editors to pass in
|
||||||
|
// a set of unsaved, modified files.
|
||||||
|
//
|
||||||
|
// Currently, only the Context.OpenFile function will respect the
|
||||||
|
// overlay. This may change in the future.
|
||||||
|
func OverlayContext(orig *build.Context, overlay map[string][]byte) *build.Context {
|
||||||
|
// TODO(dominikh): Implement IsDir, HasSubdir and ReadDir
|
||||||
|
|
||||||
|
rc := func(data []byte) (io.ReadCloser, error) {
|
||||||
|
return ioutil.NopCloser(bytes.NewBuffer(data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
copy := *orig // make a copy
|
||||||
|
ctxt := ©
|
||||||
|
ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
|
||||||
|
// Fast path: names match exactly.
|
||||||
|
if content, ok := overlay[path]; ok {
|
||||||
|
return rc(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path: check for same file under a different
|
||||||
|
// alias, perhaps due to a symbolic link.
|
||||||
|
for filename, content := range overlay {
|
||||||
|
if sameFile(path, filename) {
|
||||||
|
return rc(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OpenFile(orig, path)
|
||||||
|
}
|
||||||
|
return ctxt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOverlayArchive parses an archive containing Go files and their
|
||||||
|
// contents. The result is intended to be used with OverlayContext.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Archive format
|
||||||
|
//
|
||||||
|
// The archive consists of a series of files. Each file consists of a
|
||||||
|
// name, a decimal file size and the file contents, separated by
|
||||||
|
// newlines. No newline follows after the file contents.
|
||||||
|
func ParseOverlayArchive(archive io.Reader) (map[string][]byte, error) {
|
||||||
|
overlay := make(map[string][]byte)
|
||||||
|
r := bufio.NewReader(archive)
|
||||||
|
for {
|
||||||
|
// Read file name.
|
||||||
|
filename, err := r.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break // OK
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("reading archive file name: %v", err)
|
||||||
|
}
|
||||||
|
filename = filepath.Clean(strings.TrimSpace(filename))
|
||||||
|
|
||||||
|
// Read file size.
|
||||||
|
sz, err := r.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading size of archive file %s: %v", filename, err)
|
||||||
|
}
|
||||||
|
sz = strings.TrimSpace(sz)
|
||||||
|
size, err := strconv.ParseUint(sz, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing size of archive file %s: %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content.
|
||||||
|
content := make([]byte, size)
|
||||||
|
if _, err := io.ReadFull(r, content); err != nil {
|
||||||
|
return nil, fmt.Errorf("reading archive file %s: %v", filename, err)
|
||||||
|
}
|
||||||
|
overlay[filename] = content
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlay, nil
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2015 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 buildutil
|
||||||
|
|
||||||
|
// This logic was copied from stringsFlag from $GOROOT/src/cmd/go/build.go.
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const TagsFlagDoc = "a list of `build tags` to consider satisfied during the build. " +
|
||||||
|
"For more information about build tags, see the description of " +
|
||||||
|
"build constraints in the documentation for the go/build package"
|
||||||
|
|
||||||
|
// TagsFlag is an implementation of the flag.Value and flag.Getter interfaces that parses
|
||||||
|
// a flag value in the same manner as go build's -tags flag and
|
||||||
|
// populates a []string slice.
|
||||||
|
//
|
||||||
|
// See $GOROOT/src/go/build/doc.go for description of build tags.
|
||||||
|
// See $GOROOT/src/cmd/go/doc.go for description of 'go build -tags' flag.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc)
|
||||||
|
type TagsFlag []string
|
||||||
|
|
||||||
|
func (v *TagsFlag) Set(s string) error {
|
||||||
|
var err error
|
||||||
|
*v, err = splitQuotedFields(s)
|
||||||
|
if *v == nil {
|
||||||
|
*v = []string{}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TagsFlag) Get() interface{} { return *v }
|
||||||
|
|
||||||
|
func splitQuotedFields(s string) ([]string, error) {
|
||||||
|
// Split fields allowing '' or "" around elements.
|
||||||
|
// Quotes further inside the string do not count.
|
||||||
|
var f []string
|
||||||
|
for len(s) > 0 {
|
||||||
|
for len(s) > 0 && isSpaceByte(s[0]) {
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
if len(s) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Accepted quoted string. No unescaping inside.
|
||||||
|
if s[0] == '"' || s[0] == '\'' {
|
||||||
|
quote := s[0]
|
||||||
|
s = s[1:]
|
||||||
|
i := 0
|
||||||
|
for i < len(s) && s[i] != quote {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i >= len(s) {
|
||||||
|
return nil, fmt.Errorf("unterminated %c string", quote)
|
||||||
|
}
|
||||||
|
f = append(f, s[:i])
|
||||||
|
s = s[i+1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for i < len(s) && !isSpaceByte(s[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
f = append(f, s[:i])
|
||||||
|
s = s[i:]
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TagsFlag) String() string {
|
||||||
|
return "<tagsFlag>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpaceByte(c byte) bool {
|
||||||
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
// Copyright 2014 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 buildutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/build"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseFile behaves like parser.ParseFile,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
//
|
||||||
|
// If file is not absolute (as defined by IsAbsPath), the (dir, file)
|
||||||
|
// components are joined using JoinPath; dir must be absolute.
|
||||||
|
//
|
||||||
|
// The displayPath function, if provided, is used to transform the
|
||||||
|
// filename that will be attached to the ASTs.
|
||||||
|
//
|
||||||
|
// TODO(adonovan): call this from go/loader.parseFiles when the tree thaws.
|
||||||
|
//
|
||||||
|
func ParseFile(fset *token.FileSet, ctxt *build.Context, displayPath func(string) string, dir string, file string, mode parser.Mode) (*ast.File, error) {
|
||||||
|
if !IsAbsPath(ctxt, file) {
|
||||||
|
file = JoinPath(ctxt, dir, file)
|
||||||
|
}
|
||||||
|
rd, err := OpenFile(ctxt, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rd.Close() // ignore error
|
||||||
|
if displayPath != nil {
|
||||||
|
file = displayPath(file)
|
||||||
|
}
|
||||||
|
return parser.ParseFile(fset, file, rd, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainingPackage returns the package containing filename.
|
||||||
|
//
|
||||||
|
// If filename is not absolute, it is interpreted relative to working directory dir.
|
||||||
|
// All I/O is via the build context's file system interface, if any.
|
||||||
|
//
|
||||||
|
// The '...Files []string' fields of the resulting build.Package are not
|
||||||
|
// populated (build.FindOnly mode).
|
||||||
|
//
|
||||||
|
func ContainingPackage(ctxt *build.Context, dir, filename string) (*build.Package, error) {
|
||||||
|
if !IsAbsPath(ctxt, filename) {
|
||||||
|
filename = JoinPath(ctxt, dir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must not assume the file tree uses
|
||||||
|
// "/" always,
|
||||||
|
// `\` always,
|
||||||
|
// or os.PathSeparator (which varies by platform),
|
||||||
|
// but to make any progress, we are forced to assume that
|
||||||
|
// paths will not use `\` unless the PathSeparator
|
||||||
|
// is also `\`, thus we can rely on filepath.ToSlash for some sanity.
|
||||||
|
|
||||||
|
dirSlash := path.Dir(filepath.ToSlash(filename)) + "/"
|
||||||
|
|
||||||
|
// We assume that no source root (GOPATH[i] or GOROOT) contains any other.
|
||||||
|
for _, srcdir := range ctxt.SrcDirs() {
|
||||||
|
srcdirSlash := filepath.ToSlash(srcdir) + "/"
|
||||||
|
if importPath, ok := HasSubdir(ctxt, srcdirSlash, dirSlash); ok {
|
||||||
|
return ctxt.Import(importPath, dir, build.FindOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("can't find package containing %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Effective methods of file system interface -------------------------
|
||||||
|
|
||||||
|
// (go/build.Context defines these as methods, but does not export them.)
|
||||||
|
|
||||||
|
// hasSubdir calls ctxt.HasSubdir (if not nil) or else uses
|
||||||
|
// the local file system to answer the question.
|
||||||
|
func HasSubdir(ctxt *build.Context, root, dir string) (rel string, ok bool) {
|
||||||
|
if f := ctxt.HasSubdir; f != nil {
|
||||||
|
return f(root, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try using paths we received.
|
||||||
|
if rel, ok = hasSubdir(root, dir); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try expanding symlinks and comparing
|
||||||
|
// expanded against unexpanded and
|
||||||
|
// expanded against expanded.
|
||||||
|
rootSym, _ := filepath.EvalSymlinks(root)
|
||||||
|
dirSym, _ := filepath.EvalSymlinks(dir)
|
||||||
|
|
||||||
|
if rel, ok = hasSubdir(rootSym, dir); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rel, ok = hasSubdir(root, dirSym); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return hasSubdir(rootSym, dirSym)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSubdir(root, dir string) (rel string, ok bool) {
|
||||||
|
const sep = string(filepath.Separator)
|
||||||
|
root = filepath.Clean(root)
|
||||||
|
if !strings.HasSuffix(root, sep) {
|
||||||
|
root += sep
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
if !strings.HasPrefix(dir, root) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.ToSlash(dir[len(root):]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists returns true if the specified file exists,
|
||||||
|
// using the build context's file system interface.
|
||||||
|
func FileExists(ctxt *build.Context, path string) bool {
|
||||||
|
if ctxt.OpenFile != nil {
|
||||||
|
r, err := ctxt.OpenFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.Close() // ignore error
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFile behaves like os.Open,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
func OpenFile(ctxt *build.Context, path string) (io.ReadCloser, error) {
|
||||||
|
if ctxt.OpenFile != nil {
|
||||||
|
return ctxt.OpenFile(path)
|
||||||
|
}
|
||||||
|
return os.Open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAbsPath behaves like filepath.IsAbs,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
func IsAbsPath(ctxt *build.Context, path string) bool {
|
||||||
|
if ctxt.IsAbsPath != nil {
|
||||||
|
return ctxt.IsAbsPath(path)
|
||||||
|
}
|
||||||
|
return filepath.IsAbs(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinPath behaves like filepath.Join,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
func JoinPath(ctxt *build.Context, path ...string) string {
|
||||||
|
if ctxt.JoinPath != nil {
|
||||||
|
return ctxt.JoinPath(path...)
|
||||||
|
}
|
||||||
|
return filepath.Join(path...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir behaves like os.Stat plus IsDir,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
func IsDir(ctxt *build.Context, path string) bool {
|
||||||
|
if ctxt.IsDir != nil {
|
||||||
|
return ctxt.IsDir(path)
|
||||||
|
}
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
return err == nil && fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir behaves like ioutil.ReadDir,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
func ReadDir(ctxt *build.Context, path string) ([]os.FileInfo, error) {
|
||||||
|
if ctxt.ReadDir != nil {
|
||||||
|
return ctxt.ReadDir(path)
|
||||||
|
}
|
||||||
|
return ioutil.ReadDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitPathList behaves like filepath.SplitList,
|
||||||
|
// but uses the build context's file system interface, if any.
|
||||||
|
func SplitPathList(ctxt *build.Context, s string) []string {
|
||||||
|
if ctxt.SplitPathList != nil {
|
||||||
|
return ctxt.SplitPathList(s)
|
||||||
|
}
|
||||||
|
return filepath.SplitList(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sameFile returns true if x and y have the same basename and denote
|
||||||
|
// the same file.
|
||||||
|
//
|
||||||
|
func sameFile(x, y string) bool {
|
||||||
|
if path.Clean(x) == path.Clean(y) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if filepath.Base(x) == filepath.Base(y) { // (optimisation)
|
||||||
|
if xi, err := os.Stat(x); err == nil {
|
||||||
|
if yi, err := os.Stat(y); err == nil {
|
||||||
|
return os.SameFile(xi, yi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
// Copyright 2013 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 cgo handles cgo preprocessing of files containing `import "C"`.
|
||||||
|
//
|
||||||
|
// DESIGN
|
||||||
|
//
|
||||||
|
// The approach taken is to run the cgo processor on the package's
|
||||||
|
// CgoFiles and parse the output, faking the filenames of the
|
||||||
|
// resulting ASTs so that the synthetic file containing the C types is
|
||||||
|
// called "C" (e.g. "~/go/src/net/C") and the preprocessed files
|
||||||
|
// have their original names (e.g. "~/go/src/net/cgo_unix.go"),
|
||||||
|
// not the names of the actual temporary files.
|
||||||
|
//
|
||||||
|
// The advantage of this approach is its fidelity to 'go build'. The
|
||||||
|
// downside is that the token.Position.Offset for each AST node is
|
||||||
|
// incorrect, being an offset within the temporary file. Line numbers
|
||||||
|
// should still be correct because of the //line comments.
|
||||||
|
//
|
||||||
|
// The logic of this file is mostly plundered from the 'go build'
|
||||||
|
// tool, which also invokes the cgo preprocessor.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// REJECTED ALTERNATIVE
|
||||||
|
//
|
||||||
|
// An alternative approach that we explored is to extend go/types'
|
||||||
|
// Importer mechanism to provide the identity of the importing package
|
||||||
|
// so that each time `import "C"` appears it resolves to a different
|
||||||
|
// synthetic package containing just the objects needed in that case.
|
||||||
|
// The loader would invoke cgo but parse only the cgo_types.go file
|
||||||
|
// defining the package-level objects, discarding the other files
|
||||||
|
// resulting from preprocessing.
|
||||||
|
//
|
||||||
|
// The benefit of this approach would have been that source-level
|
||||||
|
// syntax information would correspond exactly to the original cgo
|
||||||
|
// file, with no preprocessing involved, making source tools like
|
||||||
|
// godoc, guru, and eg happy. However, the approach was rejected
|
||||||
|
// due to the additional complexity it would impose on go/types. (It
|
||||||
|
// made for a beautiful demo, though.)
|
||||||
|
//
|
||||||
|
// cgo files, despite their *.go extension, are not legal Go source
|
||||||
|
// files per the specification since they may refer to unexported
|
||||||
|
// members of package "C" such as C.int. Also, a function such as
|
||||||
|
// C.getpwent has in effect two types, one matching its C type and one
|
||||||
|
// which additionally returns (errno C.int). The cgo preprocessor
|
||||||
|
// uses name mangling to distinguish these two functions in the
|
||||||
|
// processed code, but go/types would need to duplicate this logic in
|
||||||
|
// its handling of function calls, analogous to the treatment of map
|
||||||
|
// lookups in which y=m[k] and y,ok=m[k] are both legal.
|
||||||
|
|
||||||
|
package cgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/build"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
exec "golang.org/x/sys/execabs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessFiles invokes the cgo preprocessor on bp.CgoFiles, parses
|
||||||
|
// the output and returns the resulting ASTs.
|
||||||
|
//
|
||||||
|
func ProcessFiles(bp *build.Package, fset *token.FileSet, DisplayPath func(path string) string, mode parser.Mode) ([]*ast.File, error) {
|
||||||
|
tmpdir, err := ioutil.TempDir("", strings.Replace(bp.ImportPath, "/", "_", -1)+"_C")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
|
||||||
|
pkgdir := bp.Dir
|
||||||
|
if DisplayPath != nil {
|
||||||
|
pkgdir = DisplayPath(pkgdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
cgoFiles, cgoDisplayFiles, err := Run(bp, pkgdir, tmpdir, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var files []*ast.File
|
||||||
|
for i := range cgoFiles {
|
||||||
|
rd, err := os.Open(cgoFiles[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
display := filepath.Join(bp.Dir, cgoDisplayFiles[i])
|
||||||
|
f, err := parser.ParseFile(fset, display, rd, mode)
|
||||||
|
rd.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var cgoRe = regexp.MustCompile(`[/\\:]`)
|
||||||
|
|
||||||
|
// Run invokes the cgo preprocessor on bp.CgoFiles and returns two
|
||||||
|
// lists of files: the resulting processed files (in temporary
|
||||||
|
// directory tmpdir) and the corresponding names of the unprocessed files.
|
||||||
|
//
|
||||||
|
// Run is adapted from (*builder).cgo in
|
||||||
|
// $GOROOT/src/cmd/go/build.go, but these features are unsupported:
|
||||||
|
// Objective C, CGOPKGPATH, CGO_FLAGS.
|
||||||
|
//
|
||||||
|
// If useabs is set to true, absolute paths of the bp.CgoFiles will be passed in
|
||||||
|
// to the cgo preprocessor. This in turn will set the // line comments
|
||||||
|
// referring to those files to use absolute paths. This is needed for
|
||||||
|
// go/packages using the legacy go list support so it is able to find
|
||||||
|
// the original files.
|
||||||
|
func Run(bp *build.Package, pkgdir, tmpdir string, useabs bool) (files, displayFiles []string, err error) {
|
||||||
|
cgoCPPFLAGS, _, _, _ := cflags(bp, true)
|
||||||
|
_, cgoexeCFLAGS, _, _ := cflags(bp, false)
|
||||||
|
|
||||||
|
if len(bp.CgoPkgConfig) > 0 {
|
||||||
|
pcCFLAGS, err := pkgConfigFlags(bp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
cgoCPPFLAGS = append(cgoCPPFLAGS, pcCFLAGS...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows including _cgo_export.h from .[ch] files in the package.
|
||||||
|
cgoCPPFLAGS = append(cgoCPPFLAGS, "-I", tmpdir)
|
||||||
|
|
||||||
|
// _cgo_gotypes.go (displayed "C") contains the type definitions.
|
||||||
|
files = append(files, filepath.Join(tmpdir, "_cgo_gotypes.go"))
|
||||||
|
displayFiles = append(displayFiles, "C")
|
||||||
|
for _, fn := range bp.CgoFiles {
|
||||||
|
// "foo.cgo1.go" (displayed "foo.go") is the processed Go source.
|
||||||
|
f := cgoRe.ReplaceAllString(fn[:len(fn)-len("go")], "_")
|
||||||
|
files = append(files, filepath.Join(tmpdir, f+"cgo1.go"))
|
||||||
|
displayFiles = append(displayFiles, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cgoflags []string
|
||||||
|
if bp.Goroot && bp.ImportPath == "runtime/cgo" {
|
||||||
|
cgoflags = append(cgoflags, "-import_runtime_cgo=false")
|
||||||
|
}
|
||||||
|
if bp.Goroot && bp.ImportPath == "runtime/race" || bp.ImportPath == "runtime/cgo" {
|
||||||
|
cgoflags = append(cgoflags, "-import_syscall=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cgoFiles []string = bp.CgoFiles
|
||||||
|
if useabs {
|
||||||
|
cgoFiles = make([]string, len(bp.CgoFiles))
|
||||||
|
for i := range cgoFiles {
|
||||||
|
cgoFiles[i] = filepath.Join(pkgdir, bp.CgoFiles[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := stringList(
|
||||||
|
"go", "tool", "cgo", "-objdir", tmpdir, cgoflags, "--",
|
||||||
|
cgoCPPFLAGS, cgoexeCFLAGS, cgoFiles,
|
||||||
|
)
|
||||||
|
if false {
|
||||||
|
log.Printf("Running cgo for package %q: %s (dir=%s)", bp.ImportPath, args, pkgdir)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Dir = pkgdir
|
||||||
|
cmd.Env = append(os.Environ(), "PWD="+pkgdir)
|
||||||
|
cmd.Stdout = os.Stderr
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cgo failed: %s: %s", args, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, displayFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- unmodified from 'go build' ---------------------------------------
|
||||||
|
|
||||||
|
// Return the flags to use when invoking the C or C++ compilers, or cgo.
|
||||||
|
func cflags(p *build.Package, def bool) (cppflags, cflags, cxxflags, ldflags []string) {
|
||||||
|
var defaults string
|
||||||
|
if def {
|
||||||
|
defaults = "-g -O2"
|
||||||
|
}
|
||||||
|
|
||||||
|
cppflags = stringList(envList("CGO_CPPFLAGS", ""), p.CgoCPPFLAGS)
|
||||||
|
cflags = stringList(envList("CGO_CFLAGS", defaults), p.CgoCFLAGS)
|
||||||
|
cxxflags = stringList(envList("CGO_CXXFLAGS", defaults), p.CgoCXXFLAGS)
|
||||||
|
ldflags = stringList(envList("CGO_LDFLAGS", defaults), p.CgoLDFLAGS)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// envList returns the value of the given environment variable broken
|
||||||
|
// into fields, using the default value when the variable is empty.
|
||||||
|
func envList(key, def string) []string {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
if v == "" {
|
||||||
|
v = def
|
||||||
|
}
|
||||||
|
return strings.Fields(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringList's arguments should be a sequence of string or []string values.
|
||||||
|
// stringList flattens them into a single []string.
|
||||||
|
func stringList(args ...interface{}) []string {
|
||||||
|
var x []string
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg := arg.(type) {
|
||||||
|
case []string:
|
||||||
|
x = append(x, arg...)
|
||||||
|
case string:
|
||||||
|
x = append(x, arg)
|
||||||
|
default:
|
||||||
|
panic("stringList: invalid argument")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2013 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 cgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go/build"
|
||||||
|
exec "golang.org/x/sys/execabs"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pkgConfig runs pkg-config with the specified arguments and returns the flags it prints.
|
||||||
|
func pkgConfig(mode string, pkgs []string) (flags []string, err error) {
|
||||||
|
cmd := exec.Command("pkg-config", append([]string{mode}, pkgs...)...)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
s := fmt.Sprintf("%s failed: %v", strings.Join(cmd.Args, " "), err)
|
||||||
|
if len(out) > 0 {
|
||||||
|
s = fmt.Sprintf("%s: %s", s, out)
|
||||||
|
}
|
||||||
|
return nil, errors.New(s)
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
flags = strings.Fields(string(out))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkgConfigFlags calls pkg-config if needed and returns the cflags
|
||||||
|
// needed to build the package.
|
||||||
|
func pkgConfigFlags(p *build.Package) (cflags []string, err error) {
|
||||||
|
if len(p.CgoPkgConfig) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return pkgConfig("--cflags", p.CgoPkgConfig)
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
// Copyright 2015 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 loader loads a complete Go program from source code, parsing
|
||||||
|
// and type-checking the initial packages plus their transitive closure
|
||||||
|
// of dependencies. The ASTs and the derived facts are retained for
|
||||||
|
// later use.
|
||||||
|
//
|
||||||
|
// Deprecated: This is an older API and does not have support
|
||||||
|
// for modules. Use golang.org/x/tools/go/packages instead.
|
||||||
|
//
|
||||||
|
// The package defines two primary types: Config, which specifies a
|
||||||
|
// set of initial packages to load and various other options; and
|
||||||
|
// Program, which is the result of successfully loading the packages
|
||||||
|
// specified by a configuration.
|
||||||
|
//
|
||||||
|
// The configuration can be set directly, but *Config provides various
|
||||||
|
// convenience methods to simplify the common cases, each of which can
|
||||||
|
// be called any number of times. Finally, these are followed by a
|
||||||
|
// call to Load() to actually load and type-check the program.
|
||||||
|
//
|
||||||
|
// var conf loader.Config
|
||||||
|
//
|
||||||
|
// // Use the command-line arguments to specify
|
||||||
|
// // a set of initial packages to load from source.
|
||||||
|
// // See FromArgsUsage for help.
|
||||||
|
// rest, err := conf.FromArgs(os.Args[1:], wantTests)
|
||||||
|
//
|
||||||
|
// // Parse the specified files and create an ad hoc package with path "foo".
|
||||||
|
// // All files must have the same 'package' declaration.
|
||||||
|
// conf.CreateFromFilenames("foo", "foo.go", "bar.go")
|
||||||
|
//
|
||||||
|
// // Create an ad hoc package with path "foo" from
|
||||||
|
// // the specified already-parsed files.
|
||||||
|
// // All ASTs must have the same 'package' declaration.
|
||||||
|
// conf.CreateFromFiles("foo", parsedFiles)
|
||||||
|
//
|
||||||
|
// // Add "runtime" to the set of packages to be loaded.
|
||||||
|
// conf.Import("runtime")
|
||||||
|
//
|
||||||
|
// // Adds "fmt" and "fmt_test" to the set of packages
|
||||||
|
// // to be loaded. "fmt" will include *_test.go files.
|
||||||
|
// conf.ImportWithTests("fmt")
|
||||||
|
//
|
||||||
|
// // Finally, load all the packages specified by the configuration.
|
||||||
|
// prog, err := conf.Load()
|
||||||
|
//
|
||||||
|
// See examples_test.go for examples of API usage.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// CONCEPTS AND TERMINOLOGY
|
||||||
|
//
|
||||||
|
// The WORKSPACE is the set of packages accessible to the loader. The
|
||||||
|
// workspace is defined by Config.Build, a *build.Context. The
|
||||||
|
// default context treats subdirectories of $GOROOT and $GOPATH as
|
||||||
|
// packages, but this behavior may be overridden.
|
||||||
|
//
|
||||||
|
// An AD HOC package is one specified as a set of source files on the
|
||||||
|
// command line. In the simplest case, it may consist of a single file
|
||||||
|
// such as $GOROOT/src/net/http/triv.go.
|
||||||
|
//
|
||||||
|
// EXTERNAL TEST packages are those comprised of a set of *_test.go
|
||||||
|
// files all with the same 'package foo_test' declaration, all in the
|
||||||
|
// same directory. (go/build.Package calls these files XTestFiles.)
|
||||||
|
//
|
||||||
|
// An IMPORTABLE package is one that can be referred to by some import
|
||||||
|
// spec. Every importable package is uniquely identified by its
|
||||||
|
// PACKAGE PATH or just PATH, a string such as "fmt", "encoding/json",
|
||||||
|
// or "cmd/vendor/golang.org/x/arch/x86/x86asm". A package path
|
||||||
|
// typically denotes a subdirectory of the workspace.
|
||||||
|
//
|
||||||
|
// An import declaration uses an IMPORT PATH to refer to a package.
|
||||||
|
// Most import declarations use the package path as the import path.
|
||||||
|
//
|
||||||
|
// Due to VENDORING (https://golang.org/s/go15vendor), the
|
||||||
|
// interpretation of an import path may depend on the directory in which
|
||||||
|
// it appears. To resolve an import path to a package path, go/build
|
||||||
|
// must search the enclosing directories for a subdirectory named
|
||||||
|
// "vendor".
|
||||||
|
//
|
||||||
|
// ad hoc packages and external test packages are NON-IMPORTABLE. The
|
||||||
|
// path of an ad hoc package is inferred from the package
|
||||||
|
// declarations of its files and is therefore not a unique package key.
|
||||||
|
// For example, Config.CreatePkgs may specify two initial ad hoc
|
||||||
|
// packages, both with path "main".
|
||||||
|
//
|
||||||
|
// An AUGMENTED package is an importable package P plus all the
|
||||||
|
// *_test.go files with same 'package foo' declaration as P.
|
||||||
|
// (go/build.Package calls these files TestFiles.)
|
||||||
|
//
|
||||||
|
// The INITIAL packages are those specified in the configuration. A
|
||||||
|
// DEPENDENCY is a package loaded to satisfy an import in an initial
|
||||||
|
// package or another dependency.
|
||||||
|
//
|
||||||
|
package loader
|
||||||
|
|
||||||
|
// IMPLEMENTATION NOTES
|
||||||
|
//
|
||||||
|
// 'go test', in-package test files, and import cycles
|
||||||
|
// ---------------------------------------------------
|
||||||
|
//
|
||||||
|
// An external test package may depend upon members of the augmented
|
||||||
|
// package that are not in the unaugmented package, such as functions
|
||||||
|
// that expose internals. (See bufio/export_test.go for an example.)
|
||||||
|
// So, the loader must ensure that for each external test package
|
||||||
|
// it loads, it also augments the corresponding non-test package.
|
||||||
|
//
|
||||||
|
// The import graph over n unaugmented packages must be acyclic; the
|
||||||
|
// import graph over n-1 unaugmented packages plus one augmented
|
||||||
|
// package must also be acyclic. ('go test' relies on this.) But the
|
||||||
|
// import graph over n augmented packages may contain cycles.
|
||||||
|
//
|
||||||
|
// First, all the (unaugmented) non-test packages and their
|
||||||
|
// dependencies are imported in the usual way; the loader reports an
|
||||||
|
// error if it detects an import cycle.
|
||||||
|
//
|
||||||
|
// Then, each package P for which testing is desired is augmented by
|
||||||
|
// the list P' of its in-package test files, by calling
|
||||||
|
// (*types.Checker).Files. This arrangement ensures that P' may
|
||||||
|
// reference definitions within P, but P may not reference definitions
|
||||||
|
// within P'. Furthermore, P' may import any other package, including
|
||||||
|
// ones that depend upon P, without an import cycle error.
|
||||||
|
//
|
||||||
|
// Consider two packages A and B, both of which have lists of
|
||||||
|
// in-package test files we'll call A' and B', and which have the
|
||||||
|
// following import graph edges:
|
||||||
|
// B imports A
|
||||||
|
// B' imports A
|
||||||
|
// A' imports B
|
||||||
|
// This last edge would be expected to create an error were it not
|
||||||
|
// for the special type-checking discipline above.
|
||||||
|
// Cycles of size greater than two are possible. For example:
|
||||||
|
// compress/bzip2/bzip2_test.go (package bzip2) imports "io/ioutil"
|
||||||
|
// io/ioutil/tempfile_test.go (package ioutil) imports "regexp"
|
||||||
|
// regexp/exec_test.go (package regexp) imports "compress/bzip2"
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Concurrency
|
||||||
|
// -----------
|
||||||
|
//
|
||||||
|
// Let us define the import dependency graph as follows. Each node is a
|
||||||
|
// list of files passed to (Checker).Files at once. Many of these lists
|
||||||
|
// are the production code of an importable Go package, so those nodes
|
||||||
|
// are labelled by the package's path. The remaining nodes are
|
||||||
|
// ad hoc packages and lists of in-package *_test.go files that augment
|
||||||
|
// an importable package; those nodes have no label.
|
||||||
|
//
|
||||||
|
// The edges of the graph represent import statements appearing within a
|
||||||
|
// file. An edge connects a node (a list of files) to the node it
|
||||||
|
// imports, which is importable and thus always labelled.
|
||||||
|
//
|
||||||
|
// Loading is controlled by this dependency graph.
|
||||||
|
//
|
||||||
|
// To reduce I/O latency, we start loading a package's dependencies
|
||||||
|
// asynchronously as soon as we've parsed its files and enumerated its
|
||||||
|
// imports (scanImports). This performs a preorder traversal of the
|
||||||
|
// import dependency graph.
|
||||||
|
//
|
||||||
|
// To exploit hardware parallelism, we type-check unrelated packages in
|
||||||
|
// parallel, where "unrelated" means not ordered by the partial order of
|
||||||
|
// the import dependency graph.
|
||||||
|
//
|
||||||
|
// We use a concurrency-safe non-blocking cache (importer.imported) to
|
||||||
|
// record the results of type-checking, whether success or failure. An
|
||||||
|
// entry is created in this cache by startLoad the first time the
|
||||||
|
// package is imported. The first goroutine to request an entry becomes
|
||||||
|
// responsible for completing the task and broadcasting completion to
|
||||||
|
// subsequent requestors, which block until then.
|
||||||
|
//
|
||||||
|
// Type checking occurs in (parallel) postorder: we cannot type-check a
|
||||||
|
// set of files until we have loaded and type-checked all of their
|
||||||
|
// immediate dependencies (and thus all of their transitive
|
||||||
|
// dependencies). If the input were guaranteed free of import cycles,
|
||||||
|
// this would be trivial: we could simply wait for completion of the
|
||||||
|
// dependencies and then invoke the typechecker.
|
||||||
|
//
|
||||||
|
// But as we saw in the 'go test' section above, some cycles in the
|
||||||
|
// import graph over packages are actually legal, so long as the
|
||||||
|
// cycle-forming edge originates in the in-package test files that
|
||||||
|
// augment the package. This explains why the nodes of the import
|
||||||
|
// dependency graph are not packages, but lists of files: the unlabelled
|
||||||
|
// nodes avoid the cycles. Consider packages A and B where B imports A
|
||||||
|
// and A's in-package tests AT import B. The naively constructed import
|
||||||
|
// graph over packages would contain a cycle (A+AT) --> B --> (A+AT) but
|
||||||
|
// the graph over lists of files is AT --> B --> A, where AT is an
|
||||||
|
// unlabelled node.
|
||||||
|
//
|
||||||
|
// Awaiting completion of the dependencies in a cyclic graph would
|
||||||
|
// deadlock, so we must materialize the import dependency graph (as
|
||||||
|
// importer.graph) and check whether each import edge forms a cycle. If
|
||||||
|
// x imports y, and the graph already contains a path from y to x, then
|
||||||
|
// there is an import cycle, in which case the processing of x must not
|
||||||
|
// wait for the completion of processing of y.
|
||||||
|
//
|
||||||
|
// When the type-checker makes a callback (doImport) to the loader for a
|
||||||
|
// given import edge, there are two possible cases. In the normal case,
|
||||||
|
// the dependency has already been completely type-checked; doImport
|
||||||
|
// does a cache lookup and returns it. In the cyclic case, the entry in
|
||||||
|
// the cache is still necessarily incomplete, indicating a cycle. We
|
||||||
|
// perform the cycle check again to obtain the error message, and return
|
||||||
|
// the error.
|
||||||
|
//
|
||||||
|
// The result of using concurrency is about a 2.5x speedup for stdlib_test.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright 2013 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 loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/ast"
|
||||||
|
"go/build"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/buildutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// We use a counting semaphore to limit
|
||||||
|
// the number of parallel I/O calls per process.
|
||||||
|
var ioLimit = make(chan bool, 10)
|
||||||
|
|
||||||
|
// parseFiles parses the Go source files within directory dir and
|
||||||
|
// returns the ASTs of the ones that could be at least partially parsed,
|
||||||
|
// along with a list of I/O and parse errors encountered.
|
||||||
|
//
|
||||||
|
// I/O is done via ctxt, which may specify a virtual file system.
|
||||||
|
// displayPath is used to transform the filenames attached to the ASTs.
|
||||||
|
//
|
||||||
|
func parseFiles(fset *token.FileSet, ctxt *build.Context, displayPath func(string) string, dir string, files []string, mode parser.Mode) ([]*ast.File, []error) {
|
||||||
|
if displayPath == nil {
|
||||||
|
displayPath = func(path string) string { return path }
|
||||||
|
}
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
n := len(files)
|
||||||
|
parsed := make([]*ast.File, n)
|
||||||
|
errors := make([]error, n)
|
||||||
|
for i, file := range files {
|
||||||
|
if !buildutil.IsAbsPath(ctxt, file) {
|
||||||
|
file = buildutil.JoinPath(ctxt, dir, file)
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int, file string) {
|
||||||
|
ioLimit <- true // wait
|
||||||
|
defer func() {
|
||||||
|
wg.Done()
|
||||||
|
<-ioLimit // signal
|
||||||
|
}()
|
||||||
|
var rd io.ReadCloser
|
||||||
|
var err error
|
||||||
|
if ctxt.OpenFile != nil {
|
||||||
|
rd, err = ctxt.OpenFile(file)
|
||||||
|
} else {
|
||||||
|
rd, err = os.Open(file)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
errors[i] = err // open failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFile may return both an AST and an error.
|
||||||
|
parsed[i], errors[i] = parser.ParseFile(fset, displayPath(file), rd, mode)
|
||||||
|
rd.Close()
|
||||||
|
}(i, file)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Eliminate nils, preserving order.
|
||||||
|
var o int
|
||||||
|
for _, f := range parsed {
|
||||||
|
if f != nil {
|
||||||
|
parsed[o] = f
|
||||||
|
o++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsed = parsed[:o]
|
||||||
|
|
||||||
|
o = 0
|
||||||
|
for _, err := range errors {
|
||||||
|
if err != nil {
|
||||||
|
errors[o] = err
|
||||||
|
o++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errors = errors[:o]
|
||||||
|
|
||||||
|
return parsed, errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanImports returns the set of all import paths from all
|
||||||
|
// import specs in the specified files.
|
||||||
|
func scanImports(files []*ast.File) map[string]bool {
|
||||||
|
imports := make(map[string]bool)
|
||||||
|
for _, f := range files {
|
||||||
|
for _, decl := range f.Decls {
|
||||||
|
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
|
||||||
|
for _, spec := range decl.Specs {
|
||||||
|
spec := spec.(*ast.ImportSpec)
|
||||||
|
|
||||||
|
// NB: do not assume the program is well-formed!
|
||||||
|
path, err := strconv.Unquote(spec.Path.Value)
|
||||||
|
if err != nil {
|
||||||
|
continue // quietly ignore the error
|
||||||
|
}
|
||||||
|
if path == "C" {
|
||||||
|
continue // skip pseudopackage
|
||||||
|
}
|
||||||
|
imports[path] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imports
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Internal helpers ----------
|
||||||
|
|
||||||
|
// TODO(adonovan): make this a method: func (*token.File) Contains(token.Pos)
|
||||||
|
func tokenFileContainsPos(f *token.File, pos token.Pos) bool {
|
||||||
|
p := int(pos)
|
||||||
|
base := f.Base()
|
||||||
|
return base <= p && p < base+f.Size()
|
||||||
|
}
|
|
@ -332,6 +332,9 @@ github.com/russross/blackfriday/v2
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/stretchr/testify/assert
|
github.com/stretchr/testify/assert
|
||||||
github.com/stretchr/testify/require
|
github.com/stretchr/testify/require
|
||||||
|
# github.com/sudarshan-reddy/go-sumtype v0.0.0-20210827105221-82eca7e5abb1
|
||||||
|
## explicit; go 1.16
|
||||||
|
github.com/sudarshan-reddy/go-sumtype
|
||||||
# github.com/urfave/cli/v2 v2.3.0 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
# github.com/urfave/cli/v2 v2.3.0 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/urfave/cli/v2
|
github.com/urfave/cli/v2
|
||||||
|
@ -419,9 +422,12 @@ golang.org/x/text/unicode/norm
|
||||||
## explicit; go 1.17
|
## explicit; go 1.17
|
||||||
golang.org/x/tools/go/ast/astutil
|
golang.org/x/tools/go/ast/astutil
|
||||||
golang.org/x/tools/go/ast/inspector
|
golang.org/x/tools/go/ast/inspector
|
||||||
|
golang.org/x/tools/go/buildutil
|
||||||
golang.org/x/tools/go/gcexportdata
|
golang.org/x/tools/go/gcexportdata
|
||||||
|
golang.org/x/tools/go/internal/cgo
|
||||||
golang.org/x/tools/go/internal/gcimporter
|
golang.org/x/tools/go/internal/gcimporter
|
||||||
golang.org/x/tools/go/internal/packagesdriver
|
golang.org/x/tools/go/internal/packagesdriver
|
||||||
|
golang.org/x/tools/go/loader
|
||||||
golang.org/x/tools/go/packages
|
golang.org/x/tools/go/packages
|
||||||
golang.org/x/tools/imports
|
golang.org/x/tools/imports
|
||||||
golang.org/x/tools/internal/event
|
golang.org/x/tools/internal/event
|
||||||
|
|
Loading…
Reference in New Issue