Files
2026-08-11 20:15:06 -04:00

216 lines
7.4 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
)
var excludedDirectories = map[string]bool{
".git": true,
".hg": true,
".svn": true,
"vendor": true,
}
func discover(ctx context.Context, paths []string) ([]string, []Diagnostic) {
if len(paths) == 0 {
paths = []string{"."}
}
filesByAbsolutePath := make(map[string]string)
var diagnostics []Diagnostic
for _, requested := range paths {
if err := ctx.Err(); err != nil {
diagnostics = append(diagnostics, diagnostic(requested, sourcePosition{Line: 1, Column: 1}, "HIM2001", "operation canceled: "+err.Error()))
break
}
clean := filepath.Clean(requested)
if symlinkParent, symlinkErr := firstSymlinkComponent(clean); symlinkErr != nil {
diagnostics = append(diagnostics, diagnostic(clean, sourcePosition{Line: 1, Column: 1}, "HIM2002", "cannot inspect path ancestry: "+symlinkErr.Error()))
continue
} else if symlinkParent != "" {
diagnostics = append(diagnostics, diagnostic(clean, sourcePosition{Line: 1, Column: 1}, "HIM2003", fmt.Sprintf("symlink paths are not followed (through %s)", symlinkParent)))
continue
}
info, err := os.Lstat(clean)
if err != nil {
diagnostics = append(diagnostics, diagnostic(clean, sourcePosition{Line: 1, Column: 1}, "HIM2002", "cannot inspect path: "+err.Error()))
continue
}
if info.Mode()&os.ModeSymlink != 0 {
diagnostics = append(diagnostics, diagnostic(clean, sourcePosition{Line: 1, Column: 1}, "HIM2003", "symlink paths are not followed"))
continue
}
if !info.IsDir() {
if filepath.Ext(clean) != ".sando" {
diagnostics = append(diagnostics, diagnostic(clean, sourcePosition{Line: 1, Column: 1}, "HIM2004", "explicit source file must use the .sando extension"))
continue
}
absolute, absoluteErr := filepath.Abs(clean)
if absoluteErr != nil {
diagnostics = append(diagnostics, diagnostic(clean, sourcePosition{Line: 1, Column: 1}, "HIM2005", "cannot resolve source path: "+absoluteErr.Error()))
continue
}
filesByAbsolutePath[absolute] = clean
continue
}
root := clean
rootInfo := info
walkErr := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
if err := ctx.Err(); err != nil {
return err
}
if walkErr != nil {
diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2006", "cannot inspect path during discovery: "+walkErr.Error()))
if entry != nil && entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if path != root && entry.IsDir() && excludedDirectories[entry.Name()] {
return filepath.SkipDir
}
if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if path != root && entry.IsDir() {
entryInfo, statErr := entry.Info()
if statErr != nil {
diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2007", "cannot inspect directory: "+statErr.Error()))
return filepath.SkipDir
}
if !sameFilesystem(rootInfo, entryInfo) {
diagnostics = append(diagnostics, Diagnostic{Path: path, Line: 1, Column: 1, Code: "HIM2901", Severity: SeverityWarning, Message: "skipped mounted filesystem boundary"})
return filepath.SkipDir
}
for _, marker := range []string{".git", ".hg", ".svn"} {
markerPath := filepath.Join(path, marker)
if _, markerErr := os.Lstat(markerPath); markerErr == nil {
return filepath.SkipDir
} else if !os.IsNotExist(markerErr) {
diagnostics = append(diagnostics, diagnostic(markerPath, sourcePosition{Line: 1, Column: 1}, "HIM2011", "cannot inspect nested VCS boundary: "+markerErr.Error()))
return filepath.SkipDir
}
}
modulePath := filepath.Join(path, "go.mod")
if moduleInfo, moduleErr := os.Lstat(modulePath); moduleErr == nil {
if !moduleInfo.Mode().IsRegular() {
diagnostics = append(diagnostics, diagnostic(modulePath, sourcePosition{Line: 1, Column: 1}, "HIM2008", "nested go.mod boundary is not a regular file; directory was skipped"))
}
return filepath.SkipDir
} else if !os.IsNotExist(moduleErr) {
diagnostics = append(diagnostics, diagnostic(modulePath, sourcePosition{Line: 1, Column: 1}, "HIM2008", "cannot inspect nested module boundary: "+moduleErr.Error()))
return filepath.SkipDir
}
}
if entry.IsDir() || filepath.Ext(entry.Name()) != ".sando" {
return nil
}
entryInfo, statErr := entry.Info()
if statErr != nil {
diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2009", "cannot inspect source: "+statErr.Error()))
return nil
}
if !entryInfo.Mode().IsRegular() {
return nil
}
absolute, absoluteErr := filepath.Abs(path)
if absoluteErr != nil {
diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2005", "cannot resolve source path: "+absoluteErr.Error()))
return nil
}
filesByAbsolutePath[absolute] = path
return nil
})
if walkErr != nil && ctx.Err() != nil {
diagnostics = append(diagnostics, diagnostic(root, sourcePosition{Line: 1, Column: 1}, "HIM2001", "operation canceled: "+ctx.Err().Error()))
}
}
absolutePaths := make([]string, 0, len(filesByAbsolutePath))
for absolute := range filesByAbsolutePath {
absolutePaths = append(absolutePaths, absolute)
}
sort.Strings(absolutePaths)
discovered := make([]string, 0, len(absolutePaths))
for _, absolute := range absolutePaths {
discovered = append(discovered, filesByAbsolutePath[absolute])
}
sort.SliceStable(discovered, func(i, j int) bool {
left, _ := filepath.Abs(discovered[i])
right, _ := filepath.Abs(discovered[j])
return filepath.ToSlash(left) < filepath.ToSlash(right)
})
sortDiagnostics(diagnostics)
return discovered, diagnostics
}
func firstSymlinkComponent(path string) (string, error) {
absolute, err := filepath.Abs(path)
if err != nil {
return "", err
}
volume := filepath.VolumeName(absolute)
remainder := strings.TrimPrefix(absolute, volume)
remainder = strings.TrimPrefix(remainder, string(filepath.Separator))
current := volume + string(filepath.Separator)
for _, component := range strings.Split(remainder, string(filepath.Separator)) {
if component == "" {
continue
}
current = filepath.Join(current, component)
info, lstatErr := os.Lstat(current)
if lstatErr != nil {
return "", lstatErr
}
if info.Mode()&os.ModeSymlink != 0 {
return current, nil
}
}
return "", nil
}
func sameFilesystem(root, candidate fs.FileInfo) bool {
rootDevice, rootOK := deviceNumber(root.Sys())
candidateDevice, candidateOK := deviceNumber(candidate.Sys())
return !rootOK || !candidateOK || rootDevice == candidateDevice
}
func deviceNumber(system any) (uint64, bool) {
if system == nil {
return 0, false
}
value := reflect.Indirect(reflect.ValueOf(system))
if !value.IsValid() || value.Kind() != reflect.Struct {
return 0, false
}
field := value.FieldByName("Dev")
if !field.IsValid() {
return 0, false
}
switch field.Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return field.Uint(), true
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
device := field.Int()
if device < 0 {
return 0, false
}
return uint64(device), true
default:
return 0, false
}
}