Files
wehub-resource-sync a06f331eb8
install-script / powershell-syntax (push) Waiting to run
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
install-script / install (macos-14) (push) Waiting to run
install-script / install (ubuntu-latest) (push) Waiting to run
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

219 lines
6.3 KiB
Go

package indexer
import (
"encoding/json"
"os"
"path/filepath"
"strings"
)
// cppTU is one C/C++ translation unit's reconstructed include search path,
// keyed in the compile-DB result by the TU's repo-relative source path.
type cppTU struct {
file string // repo-relative source path
includeDirs []string // repo-relative -I / -isystem / -iquote dirs, in order
}
// compileCommand is one entry of a compile_commands.json database.
type compileCommand struct {
Directory string `json:"directory"`
File string `json:"file"`
Command string `json:"command"`
Arguments []string `json:"arguments"`
}
// cppIncludeDirCache is the long-lived, memory-bounded LRU of per-repo-root
// compile-DB include-dir sets (bounded by GORTEX_RESOLVER_CACHE_MAX_MB).
var cppIncludeDirCache = newCppIncludeDirCache()
// loadCompileCommands parses compile_commands.json at the repo root (and any
// build*/compile_commands.json), reconstructing each TU's ordered include search
// path (the `-I` / `-isystem` / `-iquote` dirs) normalized to repo-relative slash
// paths, dropping directories outside the repo (toolchain / system). The result
// is cached per repo root, keyed on the newest compile_commands.json modtime: a
// later edit to compile_commands.json (even with no other source change) is
// re-read on the next load. clearCppIncludeDirCache also invalidates it on a full
// reindex.
func loadCompileCommands(repoRoot string) map[string]cppTU {
if repoRoot == "" {
return nil
}
mtime := compileDBMtime(repoRoot)
if c, ok := cppIncludeDirCache.get(repoRoot, mtime); ok {
return c
}
out := map[string]cppTU{}
for _, dbPath := range compileDBLocations(repoRoot) {
data, err := os.ReadFile(dbPath)
if err != nil {
continue
}
var cmds []compileCommand
if json.Unmarshal(data, &cmds) != nil {
continue
}
for _, cc := range cmds {
fileRel := repoRelPath(repoRoot, cc.Directory, cc.File)
if fileRel == "" {
continue
}
out[fileRel] = cppTU{file: fileRel, includeDirs: extractIncludeDirs(cc, repoRoot)}
}
}
cppIncludeDirCache.put(repoRoot, out, mtime)
return out
}
// compileDBMtime returns the newest modtime (unix nanoseconds) across the
// compile_commands.json files that loadCompileCommands reads for repoRoot, or 0
// when none exist. It is the cache freshness key: when this exceeds the cached
// entry's recorded mtime, the entry is reloaded.
func compileDBMtime(repoRoot string) int64 {
var newest int64
for _, dbPath := range compileDBLocations(repoRoot) {
fi, err := os.Stat(dbPath)
if err != nil {
continue
}
if m := fi.ModTime().UnixNano(); m > newest {
newest = m
}
}
return newest
}
// clearCppIncludeDirCache drops the cached include-dir set for a repo root so
// the next index re-reads compile_commands.json.
func clearCppIncludeDirCache(repoRoot string) {
cppIncludeDirCache.clear(repoRoot)
}
// compileDBLocations returns the absolute paths of the compile_commands.json
// files to consider: the repo root plus any build*/compile_commands.json.
func compileDBLocations(repoRoot string) []string {
var out []string
root := filepath.Join(repoRoot, "compile_commands.json")
if _, err := os.Stat(root); err == nil {
out = append(out, root)
}
if matches, err := filepath.Glob(filepath.Join(repoRoot, "build*", "compile_commands.json")); err == nil {
out = append(out, matches...)
}
return out
}
// extractIncludeDirs reconstructs the ordered include search path from a
// compile command, preferring the structured arguments array and falling back
// to a shlex split of the command string.
func extractIncludeDirs(cc compileCommand, repoRoot string) []string {
toks := cc.Arguments
if len(toks) == 0 && cc.Command != "" {
toks = shlexSplit(cc.Command)
}
var dirs []string
seen := map[string]bool{}
add := func(raw string) {
if rel := repoRelPath(repoRoot, cc.Directory, raw); rel != "" && !seen[rel] {
seen[rel] = true
dirs = append(dirs, rel)
}
}
for i := 0; i < len(toks); i++ {
t := toks[i]
switch {
case strings.HasPrefix(t, "-I") && len(t) > 2:
add(t[2:])
case t == "-I" && i+1 < len(toks):
i++
add(toks[i])
case (t == "-isystem" || t == "-iquote" || t == "-idirafter") && i+1 < len(toks):
i++
add(toks[i])
}
}
return dirs
}
// heuristicIncludeDirs returns the conventional C/C++ include-root search path
// for a repo that has no compile_commands.json: the conventional roots
// (include / src / inc / api / lib) that actually exist, in priority order,
// followed by any other top-level directory that directly contains a C/C++
// header. Paths are repo-relative slash paths. Feeds the resolver's ordered
// include probe so collisions still break deterministically without a DB.
func heuristicIncludeDirs(repoRoot string) []string {
if repoRoot == "" {
return nil
}
var dirs []string
seen := map[string]bool{}
add := func(d string) {
if d != "" && !seen[d] {
seen[d] = true
dirs = append(dirs, d)
}
}
for _, name := range []string{"include", "src", "inc", "api", "lib"} {
if fi, err := os.Stat(filepath.Join(repoRoot, name)); err == nil && fi.IsDir() {
add(name)
}
}
if entries, err := os.ReadDir(repoRoot); err == nil {
for _, e := range entries {
if !e.IsDir() {
continue
}
name := e.Name()
if strings.HasPrefix(name, ".") {
continue
}
if dirHasHeader(filepath.Join(repoRoot, name)) {
add(name)
}
}
}
return dirs
}
// dirHasHeader reports whether dir directly contains a C/C++ header file.
func dirHasHeader(dir string) bool {
entries, err := os.ReadDir(dir)
if err != nil {
return false
}
for _, e := range entries {
if e.IsDir() {
continue
}
switch filepath.Ext(e.Name()) {
case ".h", ".hpp", ".hh", ".hxx", ".h++":
return true
}
}
return false
}
// repoRelPath resolves p (relative to dir, or repoRoot when dir is empty)
// against the repo root, returning a clean repo-relative slash path — or ""
// when p resolves outside the repo (a toolchain / system path) or to the root
// itself.
func repoRelPath(repoRoot, dir, p string) string {
if p == "" {
return ""
}
abs := p
if !filepath.IsAbs(abs) {
base := dir
if base == "" {
base = repoRoot
}
abs = filepath.Join(base, p)
}
rel, err := filepath.Rel(repoRoot, filepath.Clean(abs))
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return ""
}
return filepath.ToSlash(rel)
}