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.
|
||||
#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)
|
||||
# Used for builds that want FIPS compilation but want the artifacts generated to still have the original name.
|
||||
BINARY_NAME := cloudflared
|
||||
|
@ -271,12 +273,8 @@ quic-deps:
|
|||
.PHONY: vet
|
||||
vet:
|
||||
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)
|
||||
# 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 ./...)
|
||||
$(GO_SUMTYPE) $$(go list -mod=vendor ./...)
|
||||
|
||||
.PHONY: 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/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
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
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
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/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
||||
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
|
||||
github.com/stretchr/testify/assert
|
||||
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
|
||||
## explicit; go 1.11
|
||||
github.com/urfave/cli/v2
|
||||
|
@ -419,9 +422,12 @@ golang.org/x/text/unicode/norm
|
|||
## explicit; go 1.17
|
||||
golang.org/x/tools/go/ast/astutil
|
||||
golang.org/x/tools/go/ast/inspector
|
||||
golang.org/x/tools/go/buildutil
|
||||
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/packagesdriver
|
||||
golang.org/x/tools/go/loader
|
||||
golang.org/x/tools/go/packages
|
||||
golang.org/x/tools/imports
|
||||
golang.org/x/tools/internal/event
|
||||
|
|
Loading…
Reference in New Issue