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

     1  // Copyright 2023 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 golang
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"io/fs"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"unicode"
    15  	"unicode/utf8"
    16  
    17  	"golang.org/x/tools/gopls/internal/protocol"
    18  )
    19  
    20  // ErrNoEmbed is returned by EmbedDefinition when no embed
    21  // directive is found at a particular position.
    22  // As such it indicates that other definitions could be worth checking.
    23  var ErrNoEmbed = errors.New("no embed directive found")
    24  
    25  var errStopWalk = errors.New("stop walk")
    26  
    27  // EmbedDefinition finds a file matching the embed directive at pos in the mapped file.
    28  // If there is no embed directive at pos, returns ErrNoEmbed.
    29  // If multiple files match the embed pattern, one is picked at random.
    30  func EmbedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) {
    31  	pattern, _ := parseEmbedDirective(m, pos)
    32  	if pattern == "" {
    33  		return nil, ErrNoEmbed
    34  	}
    35  
    36  	// Find the first matching file.
    37  	var match string
    38  	dir := filepath.Dir(m.URI.Path())
    39  	err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
    40  		if e != nil {
    41  			return e
    42  		}
    43  		rel, err := filepath.Rel(dir, abs)
    44  		if err != nil {
    45  			return err
    46  		}
    47  		ok, err := filepath.Match(pattern, rel)
    48  		if err != nil {
    49  			return err
    50  		}
    51  		if ok && !d.IsDir() {
    52  			match = abs
    53  			return errStopWalk
    54  		}
    55  		return nil
    56  	})
    57  	if err != nil && !errors.Is(err, errStopWalk) {
    58  		return nil, err
    59  	}
    60  	if match == "" {
    61  		return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir)
    62  	}
    63  
    64  	loc := protocol.Location{
    65  		URI: protocol.URIFromPath(match),
    66  		Range: protocol.Range{
    67  			Start: protocol.Position{Line: 0, Character: 0},
    68  		},
    69  	}
    70  	return []protocol.Location{loc}, nil
    71  }
    72  
    73  // parseEmbedDirective attempts to parse a go:embed directive argument at pos.
    74  // If successful it return the directive argument and its range, else zero values are returned.
    75  func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) {
    76  	lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0})
    77  	if err != nil {
    78  		return "", protocol.Range{}
    79  	}
    80  	lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0})
    81  	if err != nil {
    82  		return "", protocol.Range{}
    83  	}
    84  
    85  	text := string(m.Content[lineStart:lineEnd])
    86  	if !strings.HasPrefix(text, "//go:embed") {
    87  		return "", protocol.Range{}
    88  	}
    89  	text = text[len("//go:embed"):]
    90  	offset := lineStart + len("//go:embed")
    91  
    92  	// Find the first pattern in text that covers the offset of the pos we are looking for.
    93  	findOffset, err := m.PositionOffset(pos)
    94  	if err != nil {
    95  		return "", protocol.Range{}
    96  	}
    97  	patterns, err := parseGoEmbed(text, offset)
    98  	if err != nil {
    99  		return "", protocol.Range{}
   100  	}
   101  	for _, p := range patterns {
   102  		if p.startOffset <= findOffset && findOffset <= p.endOffset {
   103  			// Found our match.
   104  			rng, err := m.OffsetRange(p.startOffset, p.endOffset)
   105  			if err != nil {
   106  				return "", protocol.Range{}
   107  			}
   108  			return p.pattern, rng
   109  		}
   110  	}
   111  
   112  	return "", protocol.Range{}
   113  }
   114  
   115  type fileEmbed struct {
   116  	pattern     string
   117  	startOffset int
   118  	endOffset   int
   119  }
   120  
   121  // parseGoEmbed patterns that come after the directive.
   122  //
   123  // Copied and adapted from go/build/read.go.
   124  // Replaced token.Position with start/end offset (including quotes if present).
   125  func parseGoEmbed(args string, offset int) ([]fileEmbed, error) {
   126  	trimBytes := func(n int) {
   127  		offset += n
   128  		args = args[n:]
   129  	}
   130  	trimSpace := func() {
   131  		trim := strings.TrimLeftFunc(args, unicode.IsSpace)
   132  		trimBytes(len(args) - len(trim))
   133  	}
   134  
   135  	var list []fileEmbed
   136  	for trimSpace(); args != ""; trimSpace() {
   137  		var path string
   138  		pathOffset := offset
   139  	Switch:
   140  		switch args[0] {
   141  		default:
   142  			i := len(args)
   143  			for j, c := range args {
   144  				if unicode.IsSpace(c) {
   145  					i = j
   146  					break
   147  				}
   148  			}
   149  			path = args[:i]
   150  			trimBytes(i)
   151  
   152  		case '`':
   153  			var ok bool
   154  			path, _, ok = strings.Cut(args[1:], "`")
   155  			if !ok {
   156  				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
   157  			}
   158  			trimBytes(1 + len(path) + 1)
   159  
   160  		case '"':
   161  			i := 1
   162  			for ; i < len(args); i++ {
   163  				if args[i] == '\\' {
   164  					i++
   165  					continue
   166  				}
   167  				if args[i] == '"' {
   168  					q, err := strconv.Unquote(args[:i+1])
   169  					if err != nil {
   170  						return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1])
   171  					}
   172  					path = q
   173  					trimBytes(i + 1)
   174  					break Switch
   175  				}
   176  			}
   177  			if i >= len(args) {
   178  				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
   179  			}
   180  		}
   181  
   182  		if args != "" {
   183  			r, _ := utf8.DecodeRuneInString(args)
   184  			if !unicode.IsSpace(r) {
   185  				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
   186  			}
   187  		}
   188  		list = append(list, fileEmbed{
   189  			pattern:     path,
   190  			startOffset: pathOffset,
   191  			endOffset:   offset,
   192  		})
   193  	}
   194  	return list, nil
   195  }