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 }