loader: support "all:" prefix in //go:embed patterns

The "all:" prefix in //go:embed directives instructs the compiler to
  include hidden files (starting with "." or "_") when embedding a
  directory. Previously, the prefix was passed literally to path.Match,
  which would never match.

  Introduce an embedPattern struct that parses and validates the pattern
  at construction time via newEmbedPattern(). The "all:" prefix is
  stripped once and stored along with the glob pattern, avoiding repeated
  string prefix checks. Pattern validation is absorbed into the
  constructor, guaranteeing that any embedPattern value is valid and
  eliminating the need for separate validation loops.

  Fixes embedding with patterns like "//go:embed all:static".
This commit is contained in:
Abhishek Goyal
2026-02-21 21:25:11 +05:30
committed by deadprogram
parent 27e5baeb57
commit 44b0c79eaf
3 changed files with 71 additions and 45 deletions
+55 -41
View File
@@ -549,7 +549,7 @@ func (p *Package) extractEmbedLines(addError func(error)) {
} }
// Look for //go:embed comments. // Look for //go:embed comments.
var allPatterns []string var allPatterns []embedPattern
for _, comment := range doc.List { for _, comment := range doc.List {
if comment.Text != "//go:embed" && !strings.HasPrefix(comment.Text, "//go:embed ") { if comment.Text != "//go:embed" && !strings.HasPrefix(comment.Text, "//go:embed ") {
continue continue
@@ -577,21 +577,7 @@ func (p *Package) extractEmbedLines(addError func(error)) {
}) })
continue continue
} }
for _, pattern := range patterns { allPatterns = append(allPatterns, patterns...)
// Check that the pattern is well-formed.
// It must be valid: the Go toolchain has already
// checked for invalid patterns. But let's check
// anyway to be sure.
if _, err := path.Match(pattern, ""); err != nil {
addError(types.Error{
Fset: p.program.fset,
Pos: comment.Pos(),
Msg: "invalid pattern syntax",
})
continue
}
allPatterns = append(allPatterns, pattern)
}
} }
if len(allPatterns) != 0 { if len(allPatterns) != 0 {
@@ -624,8 +610,8 @@ func (p *Package) extractEmbedLines(addError func(error)) {
// Match all //go:embed patterns against the embed files // Match all //go:embed patterns against the embed files
// provided by `go list`. // provided by `go list`.
for _, name := range p.EmbedFiles { for _, name := range p.EmbedFiles {
for _, pattern := range allPatterns { for _, ep := range allPatterns {
if matchPattern(pattern, name) { if ep.Match(name) {
p.EmbedGlobals[globalName] = append(p.EmbedGlobals[globalName], &EmbedFile{ p.EmbedGlobals[globalName] = append(p.EmbedGlobals[globalName], &EmbedFile{
Name: name, Name: name,
NeedsData: byteSlice, NeedsData: byteSlice,
@@ -641,13 +627,37 @@ func (p *Package) extractEmbedLines(addError func(error)) {
} }
} }
// matchPattern returns true if (and only if) the given pattern would match the // embedPattern represents a valid //go:embed pattern.
// filename. The pattern could also match a parent directory of name, in which // See: https://pkg.go.dev/embed#hdr-Directives
// case hidden files do not match. type embedPattern struct {
func matchPattern(pattern, name string) bool { pattern string // glob pattern without "all:" prefix
includeHidden bool // true if "all:" prefix was present
}
// newEmbedPattern parses and validates a //go:embed pattern.
// The "all:" prefix (if present) is stripped and reflected in includeHidden.
// Returns an error if the pattern syntax is invalid.
func newEmbedPattern(s string) (embedPattern, error) {
ep := embedPattern{pattern: s}
if strings.HasPrefix(s, "all:") {
ep.pattern = s[4:]
ep.includeHidden = true
}
if _, err := path.Match(ep.pattern, ""); err != nil {
return embedPattern{}, err
}
return ep, nil
}
// Match returns true if (and only if) the pattern matches the given filename.
// The pattern could also match a parent directory of name, in which case hidden
// files do not match (unless includeHidden is true).
func (ep embedPattern) Match(name string) bool {
pattern := ep.pattern
includeHidden := ep.includeHidden
// Match this file. // Match this file.
matched, _ := path.Match(pattern, name) if matched, _ := path.Match(pattern, name); matched {
if matched {
return true return true
} }
@@ -661,17 +671,20 @@ func matchPattern(pattern, name string) bool {
dir = path.Clean(dir) dir = path.Clean(dir)
if matched, _ := path.Match(pattern, dir); matched { if matched, _ := path.Match(pattern, dir); matched {
// Pattern matches the directory. // Pattern matches the directory.
suffix := name[len(dir):] if !includeHidden {
if strings.Contains(suffix, "/_") || strings.Contains(suffix, "/.") { suffix := name[len(dir):]
// Pattern matches a hidden file. if strings.Contains(suffix, "/_") || strings.Contains(suffix, "/.") {
// Hidden files are included when listed directly as a // Pattern matches a hidden file.
// pattern, but not when they are part of a directory tree. // Hidden files are included when listed directly as a
// Source: // pattern, or when the "all:" prefix is used, but not
// > If a pattern names a directory, all files in the // when they are part of a directory tree without "all:".
// > subtree rooted at that directory are embedded // Source:
// > (recursively), except that files with names beginning // > If a pattern names a directory, all files in the
// > with . or _ are excluded. // > subtree rooted at that directory are embedded
return false // > (recursively), except that files with names beginning
// > with . or _ are excluded.
return false
}
} }
return true return true
} }
@@ -680,11 +693,12 @@ func matchPattern(pattern, name string) bool {
// parseGoEmbed is like strings.Fields but for a //go:embed line. It parses // parseGoEmbed is like strings.Fields but for a //go:embed line. It parses
// regular fields and quoted fields (that may contain spaces). // regular fields and quoted fields (that may contain spaces).
func (p *Package) parseGoEmbed(args string, pos token.Pos) (patterns []string, err error) { func (p *Package) parseGoEmbed(args string, pos token.Pos) (patterns []embedPattern, err error) {
args = strings.TrimSpace(args) args = strings.TrimSpace(args)
initialLen := len(args) initialLen := len(args)
for args != "" { for args != "" {
patternPos := pos + token.Pos(initialLen-len(args)) patternPos := pos + token.Pos(initialLen-len(args))
var pattern string
switch args[0] { switch args[0] {
case '`', '"', '\\': case '`', '"', '\\':
// Parse the next pattern using the Go scanner. // Parse the next pattern using the Go scanner.
@@ -703,8 +717,7 @@ func (p *Package) parseGoEmbed(args string, pos token.Pos) (patterns []string, e
Msg: "invalid quoted string in //go:embed", Msg: "invalid quoted string in //go:embed",
} }
} }
pattern := constant.StringVal(constant.MakeFromLiteral(lit, tok, 0)) pattern = constant.StringVal(constant.MakeFromLiteral(lit, tok, 0))
patterns = append(patterns, pattern)
args = strings.TrimLeftFunc(args[len(lit):], unicode.IsSpace) args = strings.TrimLeftFunc(args[len(lit):], unicode.IsSpace)
default: default:
// The value is just a regular value. // The value is just a regular value.
@@ -713,17 +726,18 @@ func (p *Package) parseGoEmbed(args string, pos token.Pos) (patterns []string, e
if index < 0 { if index < 0 {
index = len(args) index = len(args)
} }
pattern := args[:index] pattern = args[:index]
patterns = append(patterns, pattern)
args = strings.TrimLeftFunc(args[len(pattern):], unicode.IsSpace) args = strings.TrimLeftFunc(args[len(pattern):], unicode.IsSpace)
} }
if _, err := path.Match(patterns[len(patterns)-1], ""); err != nil { ep, err := newEmbedPattern(pattern)
if err != nil {
return nil, types.Error{ return nil, types.Error{
Fset: p.program.fset, Fset: p.program.fset,
Pos: patternPos, Pos: patternPos,
Msg: "invalid pattern syntax", Msg: "invalid pattern syntax",
} }
} }
patterns = append(patterns, ep)
} }
return patterns, nil return patterns, nil
} }
+10 -4
View File
@@ -20,6 +20,10 @@ var (
//go:embed a/b/.hidden //go:embed a/b/.hidden
var hidden string var hidden string
// A test to check that hidden files ARE included when using "all:" prefix.
//go:embed all:a
var allFiles embed.FS
var helloStringBytes = []byte(helloString) var helloStringBytes = []byte(helloString)
func main() { func main() {
@@ -27,11 +31,13 @@ func main() {
println("bytes:", strings.TrimSpace(string(helloBytes))) println("bytes:", strings.TrimSpace(string(helloBytes)))
println("[]byte(string):", strings.TrimSpace(string(helloStringBytes))) println("[]byte(string):", strings.TrimSpace(string(helloStringBytes)))
println("files:") println("files:")
readFiles(".") readFiles(".", files)
println("all:a files (should include .hidden):")
readFiles(".", allFiles)
} }
func readFiles(dir string) { func readFiles(dir string, fs embed.FS) {
entries, err := files.ReadDir(dir) entries, err := fs.ReadDir(dir)
if err != nil { if err != nil {
println(err.Error()) println(err.Error())
return return
@@ -43,7 +49,7 @@ func readFiles(dir string) {
} }
println("-", entryPath) println("-", entryPath)
if entry.IsDir() { if entry.IsDir() {
readFiles(entryPath) readFiles(entryPath, fs)
} }
} }
} }
+6
View File
@@ -7,3 +7,9 @@ files:
- a/b/bar.txt - a/b/bar.txt
- a/b/foo.txt - a/b/foo.txt
- hello.txt - hello.txt
all:a files (should include .hidden):
- a
- a/b
- a/b/.hidden
- a/b/bar.txt
- a/b/foo.txt