1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
|
// Copyright 2022 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 embeddirective
import (
_ "embed"
"go/ast"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/aliases"
"golang.org/x/tools/internal/analysisinternal"
)
//go:embed doc.go
var doc string
var Analyzer = &analysis.Analyzer{
Name: "embed",
Doc: analysisinternal.MustExtractDoc(doc, "embed"),
Run: run,
RunDespiteErrors: true,
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/embeddirective",
}
const FixCategory = "addembedimport" // recognized by gopls ApplyFix
func run(pass *analysis.Pass) (interface{}, error) {
for _, f := range pass.Files {
comments := embedDirectiveComments(f)
if len(comments) == 0 {
continue // nothing to check
}
hasEmbedImport := false
for _, imp := range f.Imports {
if imp.Path.Value == `"embed"` {
hasEmbedImport = true
break
}
}
for _, c := range comments {
pos, end := c.Pos(), c.Pos()+token.Pos(len("//go:embed"))
if !hasEmbedImport {
pass.Report(analysis.Diagnostic{
Pos: pos,
End: end,
Message: `must import "embed" when using go:embed directives`,
Category: FixCategory,
SuggestedFixes: []analysis.SuggestedFix{{
Message: `Add missing "embed" import`,
// No TextEdits => computed by a gopls command.
}},
})
}
var msg string
spec := nextVarSpec(c, f)
switch {
case spec == nil:
msg = `go:embed directives must precede a "var" declaration`
case len(spec.Names) != 1:
msg = "declarations following go:embed directives must define a single variable"
case len(spec.Values) > 0:
msg = "declarations following go:embed directives must not specify a value"
case !embeddableType(pass.TypesInfo.Defs[spec.Names[0]]):
msg = "declarations following go:embed directives must be of type string, []byte or embed.FS"
}
if msg != "" {
pass.Report(analysis.Diagnostic{
Pos: pos,
End: end,
Message: msg,
})
}
}
}
return nil, nil
}
// embedDirectiveComments returns all comments in f that contains a //go:embed directive.
func embedDirectiveComments(f *ast.File) []*ast.Comment {
comments := []*ast.Comment{}
for _, cg := range f.Comments {
for _, c := range cg.List {
if strings.HasPrefix(c.Text, "//go:embed ") {
comments = append(comments, c)
}
}
}
return comments
}
// nextVarSpec returns the ValueSpec for the variable declaration immediately following
// the go:embed comment, or nil if the next declaration is not a variable declaration.
func nextVarSpec(com *ast.Comment, f *ast.File) *ast.ValueSpec {
// Embed directives must be followed by a declaration of one variable with no value.
// There may be comments and empty lines between the directive and the declaration.
var nextDecl ast.Decl
for _, d := range f.Decls {
if com.End() < d.End() {
nextDecl = d
break
}
}
if nextDecl == nil || nextDecl.Pos() == token.NoPos {
return nil
}
decl, ok := nextDecl.(*ast.GenDecl)
if !ok {
return nil
}
if decl.Tok != token.VAR {
return nil
}
// var declarations can be both freestanding and blocks (with parenthesis).
// Only the first variable spec following the directive is interesting.
var nextSpec ast.Spec
for _, s := range decl.Specs {
if com.End() < s.End() {
nextSpec = s
break
}
}
if nextSpec == nil {
return nil
}
spec, ok := nextSpec.(*ast.ValueSpec)
if !ok {
// Invalid AST, but keep going.
return nil
}
return spec
}
// embeddableType in go:embed directives are string, []byte or embed.FS.
func embeddableType(o types.Object) bool {
if o == nil {
return false
}
// For embed.FS the underlying type is an implementation detail.
// As long as the named type resolves to embed.FS, it is OK.
if named, ok := aliases.Unalias(o.Type()).(*types.Named); ok {
obj := named.Obj()
if obj.Pkg() != nil && obj.Pkg().Path() == "embed" && obj.Name() == "FS" {
return true
}
}
switch v := o.Type().Underlying().(type) {
case *types.Basic:
return types.Identical(v, types.Typ[types.String])
case *types.Slice:
return types.Identical(v.Elem(), types.Typ[types.Byte])
}
return false
}
|