File: embeddirective.go

package info (click to toggle)
golang-golang-x-tools 1%3A0.25.0%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 22,724 kB
  • sloc: javascript: 2,027; asm: 1,645; sh: 166; yacc: 155; makefile: 49; ansic: 8
file content (195 lines) | stat: -rw-r--r-- 4,877 bytes parent folder | download | duplicates (2)
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Copyright 2023 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 golang

import (
	"errors"
	"fmt"
	"io/fs"
	"path/filepath"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"golang.org/x/tools/gopls/internal/protocol"
)

// ErrNoEmbed is returned by EmbedDefinition when no embed
// directive is found at a particular position.
// As such it indicates that other definitions could be worth checking.
var ErrNoEmbed = errors.New("no embed directive found")

var errStopWalk = errors.New("stop walk")

// embedDefinition finds a file matching the embed directive at pos in the mapped file.
// If there is no embed directive at pos, returns ErrNoEmbed.
// If multiple files match the embed pattern, one is picked at random.
func embedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) {
	pattern, _ := parseEmbedDirective(m, pos)
	if pattern == "" {
		return nil, ErrNoEmbed
	}

	// Find the first matching file.
	var match string
	dir := filepath.Dir(m.URI.Path())
	err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error {
		if e != nil {
			return e
		}
		rel, err := filepath.Rel(dir, abs)
		if err != nil {
			return err
		}
		ok, err := filepath.Match(pattern, rel)
		if err != nil {
			return err
		}
		if ok && !d.IsDir() {
			match = abs
			return errStopWalk
		}
		return nil
	})
	if err != nil && !errors.Is(err, errStopWalk) {
		return nil, err
	}
	if match == "" {
		return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir)
	}

	loc := protocol.Location{
		URI: protocol.URIFromPath(match),
		Range: protocol.Range{
			Start: protocol.Position{Line: 0, Character: 0},
		},
	}
	return []protocol.Location{loc}, nil
}

// parseEmbedDirective attempts to parse a go:embed directive argument at pos.
// If successful it return the directive argument and its range, else zero values are returned.
func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) {
	lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0})
	if err != nil {
		return "", protocol.Range{}
	}
	lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0})
	if err != nil {
		return "", protocol.Range{}
	}

	text := string(m.Content[lineStart:lineEnd])
	if !strings.HasPrefix(text, "//go:embed") {
		return "", protocol.Range{}
	}
	text = text[len("//go:embed"):]
	offset := lineStart + len("//go:embed")

	// Find the first pattern in text that covers the offset of the pos we are looking for.
	findOffset, err := m.PositionOffset(pos)
	if err != nil {
		return "", protocol.Range{}
	}
	patterns, err := parseGoEmbed(text, offset)
	if err != nil {
		return "", protocol.Range{}
	}
	for _, p := range patterns {
		if p.startOffset <= findOffset && findOffset <= p.endOffset {
			// Found our match.
			rng, err := m.OffsetRange(p.startOffset, p.endOffset)
			if err != nil {
				return "", protocol.Range{}
			}
			return p.pattern, rng
		}
	}

	return "", protocol.Range{}
}

type fileEmbed struct {
	pattern     string
	startOffset int
	endOffset   int
}

// parseGoEmbed patterns that come after the directive.
//
// Copied and adapted from go/build/read.go.
// Replaced token.Position with start/end offset (including quotes if present).
func parseGoEmbed(args string, offset int) ([]fileEmbed, error) {
	trimBytes := func(n int) {
		offset += n
		args = args[n:]
	}
	trimSpace := func() {
		trim := strings.TrimLeftFunc(args, unicode.IsSpace)
		trimBytes(len(args) - len(trim))
	}

	var list []fileEmbed
	for trimSpace(); args != ""; trimSpace() {
		var path string
		pathOffset := offset
	Switch:
		switch args[0] {
		default:
			i := len(args)
			for j, c := range args {
				if unicode.IsSpace(c) {
					i = j
					break
				}
			}
			path = args[:i]
			trimBytes(i)

		case '`':
			var ok bool
			path, _, ok = strings.Cut(args[1:], "`")
			if !ok {
				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
			}
			trimBytes(1 + len(path) + 1)

		case '"':
			i := 1
			for ; i < len(args); i++ {
				if args[i] == '\\' {
					i++
					continue
				}
				if args[i] == '"' {
					q, err := strconv.Unquote(args[:i+1])
					if err != nil {
						return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1])
					}
					path = q
					trimBytes(i + 1)
					break Switch
				}
			}
			if i >= len(args) {
				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
			}
		}

		if args != "" {
			r, _ := utf8.DecodeRuneInString(args)
			if !unicode.IsSpace(r) {
				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
			}
		}
		list = append(list, fileEmbed{
			pattern:     path,
			startOffset: pathOffset,
			endOffset:   offset,
		})
	}
	return list, nil
}