golang.org/x/tools/gopls@v0.15.3/internal/analysis/embeddirective/embeddirective.go (about)

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