package paths import ( "path" "path/filepath" "strings" "encr.dev/pkg/fns" ) type FileReader func(string) ([]byte, error) // RootedFSPath returns a new FS path. // It should typically not be used except for at parser initialization. // Use FS.Join, FS.New, or FS.Resolve instead to preserve the working dir. func RootedFSPath(wd, p string) FS { if wd == "" { panic("paths: empty wd") } else if !filepath.IsAbs(wd) { panic("paths: wd is relative") } if filepath.IsAbs(p) { return FS(filepath.Clean(p)) } else { return FS(filepath.Join(wd, p)) } } // FS represents a filesystem path. // // It is an absolute path, and is always in the OS-specific format. type FS string // ToIO returns the path for use in IO operations. func (fs FS) ToIO() string { fs.checkValid() return string(fs) } // ToDisplay returns the path in a form suitable for displaying // to the user. func (fs FS) ToDisplay() string { fs.checkValid() return string(fs) } // Resolve returns a new FS path to the given path. // If p is absolute it returns p directly, // otherwise it returns the path joined with the current path. func (fs FS) Resolve(p string) FS { fs.checkValid() if filepath.IsAbs(p) { return FS(filepath.Clean(p)) } return FS(filepath.Join(string(fs), p)) } // Join joins the path with the given elems, according to filepath.Join. func (fs FS) Join(elem ...string) FS { fs.checkValid() parts := append([]string{string(fs)}, elem...) return FS(filepath.Join(parts...)) } func (fs FS) JoinSlash(rel RelSlash) FS { return fs.Join(filepath.FromSlash(rel.ToIO())) } // Base returns the filepath.Base of the path. func (fs FS) Base() string { fs.checkValid() return filepath.Base(string(fs)) } // Dir returns the filepath.Dir of the path. func (fs FS) Dir() FS { fs.checkValid() return FS(filepath.Dir(string(fs))) } // HasPrefix reports whether fs is a descendant of other // or is equal to other. (i.e. it is the given path or a subdirectory of it) func (fs FS) HasPrefix(other FS) bool { fs.checkValid() other.checkValid() // Note: we use filepath.Rel instead of strings.HasPrefix with filepath.Abs // because that wouldn't work on case-insensitive filesystems. rel, err := filepath.Rel(string(other), string(fs)) if err != nil { return false } return filepath.IsLocal(rel) } func (fs FS) checkValid() { if fs == "" { panic("empty FS path") } } // ValidPkgPath reports whether a given module path is valid. func ValidPkgPath(p string) bool { return p != "" } // PkgPath returns a new Pkg path for p. If p is not a valid // package path it reports "", false. func PkgPath(p string) (Pkg, bool) { if !ValidPkgPath(p) { return "", false } return Pkg(p), true } func MustPkgPath(p string) Pkg { if !ValidPkgPath(p) { panic("invalid Package path") } return Pkg(p) } // Pkg represents a package path within a module. // It is always slash-separated. type Pkg string // String returns the string representation of p. func (p Pkg) String() string { return string(p) } // JoinSlash joins the path with the given elems, according to path.Join. // The elems are expected to be slash-separated, not filesystem-separated. // Use filesystem.ToSlash() to convert filesystem paths to slash-separated paths. func (p Pkg) JoinSlash(elem ...RelSlash) Pkg { p.checkValid() strs := make([]string, len(elem)+1) strs[0] = string(p) copy(strs[1:], fns.Map(elem, RelSlash.String)) return Pkg(path.Join(strs...)) // Join cleans the result } func (p Pkg) checkValid() { if p == "" { panic("invalid Pkg path") } } // LexicallyContains reports whether the given package path contains the package path p // as a "sub-package". func (p Pkg) LexicallyContains(other Pkg) bool { p.checkValid() if other == "" { return false } return p == other || strings.HasPrefix(string(other), string(p)+"/") } const stdModule = "std" // Mod represents a module path. // It is always slash-separated. type Mod string // ValidModPath reports whether a given module path is valid. func ValidModPath(p string) bool { return p != "" } // MustModPath returns a new Mod path for p. func MustModPath(p string) Mod { if !ValidModPath(p) { panic("invalid Module path") } return Mod(p) } // StdlibMod returns the Mod path representing the standard library. func StdlibMod() Mod { return stdModule } // LexicallyContains reports whether the given module path contains the package path p. // It only considers the lexical path and ignores whether there exists // a nested module that contains p. func (m Mod) LexicallyContains(p Pkg) bool { m.checkValid() if p == "" { return false } // From the spec: // A module that will never be fetched as a dependency of any other module may use // any valid package path for its module path, but must take care not to collide // with paths that may be used by the module's dependencies or the Go standard // library. The Go standard library uses package paths that do not contain a dot in // the first path element, and the `go` command does not attempt to resolve such // paths from network servers. The paths `example` and `test` are reserved for // users: they will not be used in the standard library and are suitable for use in // self-contained modules, such as those defined in tutorials or example code or // created and manipulated as part of a test. ms, ps := string(m), string(p) if m == stdModule { // Treat any dotless package path as being contained, as long as // it's not one of the reserved paths. if first, _, _ := strings.Cut(ps, "/"); strings.Contains(first, ".") { return false } else if first == "example" || first == "tests" { // Reserved; guaranteed not to be part of std return false } return true } // We can treat the module path as a package path for this purpose. return ms == ps || strings.HasPrefix(ps, ms+"/") } // RelativePathToPkg returns the relative path from the module to the package. // If the package is not contained within the module it reports "", false. func (m Mod) RelativePathToPkg(p Pkg) (relative RelSlash, ok bool) { m.checkValid() p.checkValid() if !m.LexicallyContains(p) { return "", false } // The module path is a prefix of the package path. // Remove the module path and the leading slash. if m == stdModule { return RelSlash(p), true } // Is the package path the same as the module path? if string(p) == string(m) { return ".", true } suffix, ok := strings.CutPrefix(string(p), string(m)+"/") if !ok { return "", false } return RelSlash(suffix), true } func (m Mod) Pkg(rel RelSlash) Pkg { m.checkValid() if m == stdModule { return Pkg(rel) } return Pkg(path.Join(string(m), string(rel))) } func (m Mod) checkValid() { if m == "" { panic("invalid Module path") } } // IsStdlib reports whether m represents the standard library. func (m Mod) IsStdlib() bool { return m == stdModule } // RelSlash is a relative path that is always slash-separated. type RelSlash string // ToIO converts the slash-separated path to a filesystem path // using filepath.FromSlash. func (p RelSlash) ToIO() string { return filepath.FromSlash(string(p)) } // Join joins the path with the given elems, according to path.Join. func (rel RelSlash) Join(elem ...string) RelSlash { parts := append([]string{string(rel)}, elem...) return RelSlash(path.Join(parts...)) } func (p RelSlash) String() string { return string(p) } // MainModuleRelSlash is like RelSlash, but it's always relative to the application's // main module directory. type MainModuleRelSlash string // ToIO converts the slash-separated path to a filesystem path // using filepath.FromSlash. func (p MainModuleRelSlash) ToIO(mainModDir FS) string { return mainModDir.Join(filepath.FromSlash(string(p))).ToIO() } func (p MainModuleRelSlash) String() string { return string(p) }