Documentation
¶
Overview ¶
Package projecttype detects what kind of project lives in a directory — Go module, Node app, Rust crate, Terraform stack, etc. — by checking for canonical indicator files (go.mod, package.json, Cargo.toml, *.tf, …) in the directory's own listing (not recursive).
Mirrors the FilenameMatcher pattern from internal/content but at a directory granularity. A directory can match multiple project types at once: a Go module that also ships docker-compose.yml matches both `go` and `docker-compose` simultaneously, exactly like the cross-predicate firing for file content types (PR #95).
Custom user-registered project types via CEL expressions are NOT in this MVP — a follow-up PR will add a CEL-driven extension path over the same registry.
Index ¶
- Constants
- func CollectBuildExcludes(ctx context.Context, root string) ([]string, error)
- func CollectBuildExcludesWithOptions(ctx context.Context, root string, opts FindOptions) ([]string, error)
- func DiscoveryPaths() []string
- func ExtensionsForLanguage(lang string) []string
- func IsMinified(content []byte) bool
- func IsVendored(path string) bool
- func LanguageForExt(ext string) string
- func LanguageForPath(path string) string
- func Languages() []string
- func LoadDiscovered() (int, error)
- func LoadFromFile(path string) (int, error)
- func Register(t *ProjectType)
- func RegisterVendorPattern(patterns ...string) error
- func SetCELCompiler(fn func(expr string) (DirEvaluator, error))
- type Config
- type ConfigEntry
- type ConfigIndicator
- type DirEvaluator
- type DiscoveryEntry
- type FindOptions
- type FindResult
- type FoundProject
- type Indicator
- type Match
- type ProjectResolver
- type ProjectType
- type Registry
- func (r *Registry) CollectBuildExcludes(ctx context.Context, root string) ([]string, error)
- func (r *Registry) CollectBuildExcludesWithOptions(ctx context.Context, root string, opts FindOptions) ([]string, error)
- func (r *Registry) Detect(fsys fs.FS, dir string) []Match
- func (r *Registry) Find(ctx context.Context, root string, opts FindOptions) (*FindResult, error)
- func (r *Registry) LoadDiscovered() (int, error)
- func (r *Registry) LoadFromFile(path string) (int, error)
- func (r *Registry) Register(t *ProjectType) error
- func (r *Registry) Types() []*ProjectType
- type ResolveOptions
- type SkipDirFunc
- type VendorMatcher
Constants ¶
const ConfigDirName = "file-search-on"
ConfigDirName is the per-platform user-config-dir subdirectory project-type configs live under. Joined with os.UserConfigDir() → $XDG_CONFIG_HOME/file-search-on / $HOME/Library/Application Support/file-search-on / %APPDATA%\file-search-on depending on platform.
const ConfigFileName = "project-types.yaml"
ConfigFileName is the basename project-type YAML config files use in both the user-wide and per-project search locations. Wrapped in a `.file-search-on/` directory rather than living as a dotfile at the top level so future companion configs (cache prefs, default flags, …) can sit alongside without further filename inflation.
const PerProjectDirName = ".file-search-on"
PerProjectDirName is the directory project-type configs live in when colocated with a checkout. Walked from the current working directory only — git-style ancestor walk-up is a follow-up.
Variables ¶
This section is empty.
Functions ¶
func CollectBuildExcludes ¶
CollectBuildExcludes is the package-level shortcut over DefaultRegistry.
func CollectBuildExcludesWithOptions ¶ added in v0.4.0
func CollectBuildExcludesWithOptions(ctx context.Context, root string, opts FindOptions) ([]string, error)
CollectBuildExcludesWithOptions is the package-level shortcut over DefaultRegistry.
func DiscoveryPaths ¶
func DiscoveryPaths() []string
DiscoveryPaths is the legacy plain-paths shortcut over DiscoveryEntries — kept because LoadDiscovered (and external callers) only need the path strings. New code that wants scope labels should call DiscoveryEntries directly.
func ExtensionsForLanguage ¶ added in v0.5.0
ExtensionsForLanguage returns the extensions mapped to a language id, sorted and each with a leading dot, or nil for an unknown id. The returned slice is a fresh copy the caller may modify. (langExtensions slices are sorted in place at init, so no re-sort is needed here.)
func IsMinified ¶ added in v0.6.0
IsMinified reports whether content looks minified from its shape alone — useful for bundles that carry no telltale name (e.g. a minified prism.js). It is a heuristic: content of a meaningful size whose average line length far exceeds normal source. Files under 512 bytes are never considered minified, and binary content (a NUL byte in the first 512 bytes) is rejected outright so images/PDFs/archives — which also have few newlines — aren't mistaken for minified text. Only the first maxMinifiedScan bytes are examined.
func IsVendored ¶ added in v0.6.0
IsVendored reports whether path looks like vendored/minified/third-party content, using the built-in pattern set (plus anything added via RegisterVendorPattern). For a matcher you fully control, use a VendorMatcher.
func LanguageForExt ¶ added in v0.5.0
LanguageForExt returns the language id for a single file extension, or "" when unrecognised. The argument is normalised, so "go", ".go", and ".GO" are all accepted.
func LanguageForPath ¶ added in v0.5.0
LanguageForPath returns the canonical language id for a file path, derived from its extension, or "" when the extension is not recognised. Matching is case-insensitive, so "Main.GO" and "x.R" resolve like "main.go" and "x.r".
func Languages ¶ added in v0.5.0
func Languages() []string
Languages returns the sorted set of language ids known to the detector. The returned slice is a fresh copy the caller may modify.
func LoadDiscovered ¶
LoadDiscovered is the package-level shortcut targeting DefaultRegistry(). The CLI's main() calls this before kong dispatches the subcommand (when --no-config-search isn't set).
func LoadFromFile ¶
LoadFromFile is the package-level shortcut that targets DefaultRegistry(). The CLI's --project-type-config flag uses this directly.
func Register ¶
func Register(t *ProjectType)
Register adds a project type to the package-level default registry. Panics on CEL compile failure — appropriate for init() callers (built-in types) where a bad indicator is a programming bug. Config-driven callers should use Registry.Register on DefaultRegistry() directly to get an error return.
func RegisterVendorPattern ¶ added in v0.6.0
RegisterVendorPattern adds patterns to the process-wide matcher used by IsVendored. Safe for concurrent use; returns the first compile error.
func SetCELCompiler ¶
func SetCELCompiler(fn func(expr string) (DirEvaluator, error))
SetCELCompiler installs the CEL indicator compiler. Called from celindicators' init(); apps enable CEL indicators with a blank import:
import _ "github.com/richardwooding/projectdetect/celindicators"
Types ¶
type Config ¶
type Config struct {
ProjectTypes []ConfigEntry `yaml:"project_types"`
}
Config is the YAML schema accepted by LoadFromFile. Each entry registers a new ProjectType alongside the built-ins.
project_types:
- name: my-app
description: Internal Foo app
indicators:
- cel: '"services" in subdirs && "foo.yaml" in files'
- name: helm-chart
indicators:
- has_file: Chart.yaml
- has_file: values.yaml
type ConfigEntry ¶
type ConfigEntry struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Indicators []ConfigIndicator `yaml:"indicators"`
}
ConfigEntry is one project type declared in the config file.
type ConfigIndicator ¶
type ConfigIndicator struct {
HasFile string `yaml:"has_file,omitempty"`
HasGlob string `yaml:"has_glob,omitempty"`
HasSubdirGlob string `yaml:"has_subdir_glob,omitempty"`
CEL string `yaml:"cel,omitempty"`
}
ConfigIndicator is the YAML representation of an Indicator. Exactly one of HasFile / HasGlob / HasSubdirGlob / CEL should be set per entry.
type DirEvaluator ¶
DirEvaluator evaluates a compiled directory predicate (a CELExpr indicator) against a directory's file/subdir basenames. The base package stays free of any CEL dependency by going through this interface; the concrete implementation is supplied by the optional github.com/richardwooding/projectdetect/celindicators sub-package.
type DiscoveryEntry ¶
DiscoveryEntry annotates a single discovery path with its scope ("user-wide" or "per-project") so consumers can label / order without re-deriving the meaning from the path itself.
func DiscoveryEntries ¶
func DiscoveryEntries() []DiscoveryEntry
DiscoveryEntries returns the project-type config search locations in load order (later layers register on top of earlier), each annotated with its scope. Paths whose anchor can't be resolved (UserConfigDir unset, Getwd fails) are omitted so the caller never sees an empty / suspicious entry.
type FindOptions ¶
type FindOptions struct {
// Types, when non-empty, restricts results to projects matching
// at least one of the named types. Empty means accept all.
Types []string
// Excludes is a list of basename globs that prune directories
// during the walk. Same semantics as search.Options.Excludes.
Excludes []string
// SkipDir, when non-nil, is consulted for every directory below
// root: returning true prunes that subtree. It runs in addition to
// Excludes and VCS pruning — any one of them firing prunes the
// directory. nil means no extra pruning (backward-compatible).
SkipDir SkipDirFunc
// IncludeVCSDirs, when false (the default), prunes version-control
// metadata directories (.git, .hg, .svn) from the walk — they never
// contain project roots and walking them is wasted I/O. Set true to
// restore descending into them.
IncludeVCSDirs bool
// Nested, when true, keeps walking inside a matched project
// root so nested sub-projects (monorepo workspaces, vendored
// dependencies) are also reported. Default (false) stops at the
// first match, which is the common "find me all my Go repos"
// shape.
Nested bool
// RespectGitignore parses a .gitignore at the walk root and
// prunes matching paths. Nested .gitignore files are not
// consulted (same caveat as search).
RespectGitignore bool
// Timeout bounds the walk. Zero means no timeout. On expiry,
// FindResult.Cancelled is set with the partial result.
Timeout time.Duration
}
FindOptions configures a recursive search for project roots under a starting directory.
type FindResult ¶
type FindResult struct {
Projects []FoundProject `json:"projects"`
Count int `json:"count"`
Cancelled bool `json:"cancelled,omitempty"`
CancellationReason string `json:"cancellation_reason,omitempty"`
ElapsedSeconds float64 `json:"elapsed_seconds,omitempty"`
}
FindResult is the structured output of Find.
func Find ¶
func Find(ctx context.Context, root string, opts FindOptions) (*FindResult, error)
Find is the package-level shortcut over DefaultRegistry.
type FoundProject ¶
FoundProject is one project root found during a Find walk.
type Indicator ¶
type Indicator struct {
// HasFile is a case-insensitive exact basename match. Most
// built-in project indicators are this shape (go.mod,
// package.json, Cargo.toml, pyproject.toml, Gemfile, pom.xml).
HasFile string
// HasGlob is a glob (filepath.Match) over FILE basenames. Used by
// Terraform (`*.tf`), .NET (`*.csproj`), and similar
// extension-based project signals. Directories are never matched
// — use HasSubdirGlob for those.
HasGlob string
// HasSubdirGlob is a glob (filepath.Match) over immediate
// SUBDIRECTORY basenames. Used by project types whose canonical
// marker is a directory bundle rather than a file — e.g. Xcode's
// `*.xcodeproj` / `*.xcworkspace`. HasFile / HasGlob only ever see
// files, so a directory marker can only fire through this rule.
HasSubdirGlob string
// CELExpr is a CEL expression evaluated against a directory
// context with two list-of-string variables:
//
// files — basenames of files in the inspected dir
// subdirs — basenames of immediate subdirectories
//
// Example: `"services" in subdirs && "foo.yaml" in files`. The
// expression must compile at Register() time; bad CEL fails
// the registration. Used by user-defined custom project types
// loaded from --project-type-config YAML.
CELExpr string
}
Indicator is a single match rule against a directory's contents. Exactly one of HasFile / HasGlob / HasSubdirGlob / CELExpr should be set per indicator.
type Match ¶
Match couples a matched ProjectType with the indicator that fired. Surfaced in detect_project and find_projects output so consumers can audit detection decisions.
func ResolveForPath ¶
ResolveForPath is the single-shot, no-cache, unbounded-walk-up counterpart to ProjectResolver.Resolve. Given an absolute file path, it walks up the directory chain (no root limit — terminates at the filesystem root) and returns the first ancestor that matches a registered project type plus the matched types.
Returns ("", nil) when no ancestor matches. Use this for one-off "which project does this file belong to?" lookups (e.g. the MCP resolve_project_for_path tool, the CLI which-project subcommand). For batch walks where multiple files share an ancestor, prefer ProjectResolver — it caches per-directory.
reg may be nil; nil means DefaultRegistry(). VCS metadata dirs are skipped as candidates by default — use ResolveForPathWithOptions for a SkipDir predicate or to opt back into them.
func ResolveForPathWithOptions ¶ added in v0.4.0
func ResolveForPathWithOptions(filePath string, reg *Registry, opts ResolveOptions) (string, []Match)
ResolveForPathWithOptions is ResolveForPath with caller control over which ancestor directories are considered. Because the walk is unbounded (no enclosing root), the SkipDir predicate receives the directory's full path as relPath.
reg may be nil; nil means DefaultRegistry().
type ProjectResolver ¶
type ProjectResolver struct {
// contains filtered or unexported fields
}
ProjectResolver answers "what project does this file belong to?" by walking up the file's directory chain to the nearest ancestor that matches a registered ProjectType. Cached per-directory via sync.Map so a walk of 1000 files in one project root costs one Detect call, not 1000.
Worker-safe: every concurrent search worker can call Resolve without coordination beyond the sync.Map.
func NewResolver ¶
func NewResolver(root string, reg *Registry) *ProjectResolver
NewResolver returns a resolver rooted at root. Resolve will walk up no higher than root — files at the root with no project indicators return an empty match slice.
reg may be nil; nil means DefaultRegistry(). VCS metadata dirs are pruned by default — use NewResolverWithOptions for a SkipDir predicate or to opt back into them.
func NewResolverWithOptions ¶ added in v0.4.0
func NewResolverWithOptions(root string, reg *Registry, opts ResolveOptions) *ProjectResolver
NewResolverWithOptions is NewResolver with caller control over which ancestor directories are considered during the walk-up.
func (*ProjectResolver) Resolve ¶
func (r *ProjectResolver) Resolve(filePath string) []Match
Resolve walks up from filePath's directory looking for the nearest ancestor that matches a registered project type. Returns the matched types for the closest project (alphabetically sorted by Detect), or nil if no ancestor matches up to the walk root.
Cached: each unique directory is detected at most once for the resolver's lifetime.
type ProjectType ¶
type ProjectType struct {
// Name is the stable identifier, lowercase + dashes (e.g. "go",
// "node", "java-maven"). Used as the bucket key in MCP / CLI
// output and the future is_X_project predicate root.
Name string
// Description is a short human-readable label.
Description string
// Indicators is the OR-list. The first one that matches wins
// (and is returned to the caller for "why did this match"
// debuggability).
Indicators []Indicator
// BuildExcludes is the list of canonical build-artefact
// basenames typically present in this kind of project (e.g.
// "vendor" for Go, "node_modules" for Node, "target" for Rust).
// The walker unions these into its excludes when
// search.Options.PruneBuildArtefacts is set, so a search over a
// monorepo doesn't grovel through dependency caches by default.
// Empty for project types that have no canonical artefact dir.
BuildExcludes []string
// contains filtered or unexported fields
}
ProjectType describes a kind of project and the indicators that identify it. Indicators are evaluated against a directory's own listing (basenames only — no recursion into subdirectories). Any single indicator matching is enough to count the directory as this project type (OR semantics across the list).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the registered project types. Built-in types self-register via init() into DefaultRegistry; future custom-type registration (CEL-driven) will use the same path.
func DefaultRegistry ¶
func DefaultRegistry() *Registry
DefaultRegistry returns the package-level registry — the singleton every consumer (MCP tool, CLI subcommand) uses.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty Registry — useful for tests that want isolation from the package-level defaultRegistry. Production code almost always uses DefaultRegistry().
func (*Registry) CollectBuildExcludes ¶
CollectBuildExcludes walks root and returns the sorted, deduped union of BuildExcludes from every project type detected at or below root (Nested-style — every project, not just outer roots). Used by the search walker when Options.PruneBuildArtefacts is set to pre-populate the basename excluder before the main walk starts, so e.g. `vendor/` inside a Go module is pruned even when the file-search walker would have visited it before recognising the project.
Cheap: only directories that contain at least one project indicator contribute. Empty result when no projects are detected (or the registry has no built-ins with BuildExcludes set).
Prunes VCS dirs by default (.git/.hg/.svn). To pass a SkipDir predicate or other walk options, use CollectBuildExcludesWithOptions.
func (*Registry) CollectBuildExcludesWithOptions ¶ added in v0.4.0
func (r *Registry) CollectBuildExcludesWithOptions(ctx context.Context, root string, opts FindOptions) ([]string, error)
CollectBuildExcludesWithOptions is CollectBuildExcludes with caller control over the walk. opts.Nested is forced on (CollectBuildExcludes is inherently a nested collect — every project below root contributes), but opts.SkipDir, opts.Excludes, and opts.IncludeVCSDirs are honoured so the collect walk prunes exactly the same subtrees the caller's own walker does. opts.Types is honoured too — restrict to the project types whose excludes you care about.
func (*Registry) Detect ¶
Detect inspects a single directory and returns the project types it matches. A directory can match multiple types simultaneously (e.g. a Go module with a docker-compose.yml). Returns an empty slice when no project type fires.
Reads the directory's listing once (non-recursive); HasFile / HasGlob indicators run against file basenames, HasSubdirGlob against immediate subdirectory basenames, and CELExpr indicators see both `files` and `subdirs` lists. fsys=nil uses os.ReadDir for the production filesystem path.
func (*Registry) Find ¶
func (r *Registry) Find(ctx context.Context, root string, opts FindOptions) (*FindResult, error)
Find walks root recursively and returns every directory that matches at least one project type (subject to Options.Types). By default the walker does NOT descend into matched roots (Nested=false) — set Nested=true to also surface sub-projects.
Honours ctx cancellation and Options.Timeout; on expiry the partial result is returned with Cancelled=true (same contract as the file search tool).
func (*Registry) LoadDiscovered ¶
LoadDiscovered loads every project-type config found via DiscoveryPaths into this registry, in precedence order. Missing files (most common case: the user has no custom types) are NOT errors. Returns the total number of types registered + the first real error encountered.
Real errors (YAML parse failure, bad CEL, missing indicators) are fatal — they indicate a misconfiguration the user should fix before any subcommand runs.
func (*Registry) LoadFromFile ¶
LoadFromFile parses path as YAML and registers every project type into this registry. Validates each entry (non-empty name + at least one indicator) and surfaces CEL compile errors with the offending entry's name. Returns the number of types registered.
Idempotent: calling LoadFromFile twice with the same config registers the types twice (caller's responsibility to avoid).
func (*Registry) Register ¶
func (r *Registry) Register(t *ProjectType) error
Register adds a project type to this registry. Compiles any CEL-expression indicators eagerly — bad CEL is returned as an error so callers (config loaders, tests) can surface it cleanly.
func (*Registry) Types ¶
func (r *Registry) Types() []*ProjectType
Types returns a snapshot of every registered project type, sorted by Name. Used by --list-style help output and tests.
type ResolveOptions ¶ added in v0.4.0
type ResolveOptions struct {
// SkipDir, when non-nil, is consulted for each ancestor directory:
// returning true skips it as a project-root candidate (the walk
// continues to its parent). Use it to keep a resolver in step with
// a caller's own exclusions.
SkipDir SkipDirFunc
// IncludeVCSDirs, when false (the default), skips version-control
// metadata directories (.git, .hg, .svn) as project-root
// candidates. Set true to consider them.
IncludeVCSDirs bool
}
ResolveOptions tunes which ancestor directories a resolver walk-up considers. The zero value matches the historical behaviour except that VCS metadata dirs are pruned by default (see IncludeVCSDirs).
type SkipDirFunc ¶ added in v0.4.0
SkipDirFunc reports whether a directory should be pruned from a walk (the directory and its whole subtree are skipped). It is consulted for every directory except the walk root, which is never pruned.
relPath is the directory's path relative to the walk root (e.g. "vendor", "a/node_modules"); name is its base name. For the unbounded ResolveForPathWithOptions walk-up there is no enclosing root, so relPath is the directory's full path.
Wire it into FindOptions.SkipDir or ResolveOptions.SkipDir to mirror an external walker's exclusions — so projectdetect visits exactly the same tree as the caller (e.g. file-search-on's own walkers).
type VendorMatcher ¶ added in v0.6.0
type VendorMatcher struct {
// contains filtered or unexported fields
}
VendorMatcher tests a file path against a set of patterns identifying vendored, minified, or otherwise third-party content. Matching is purely path-based (no file I/O) and runs against the slash-separated path, so it is cross-platform. The zero value is not usable; construct one with NewVendorMatcher or DefaultVendorMatcher.
func DefaultVendorMatcher ¶ added in v0.6.0
func DefaultVendorMatcher() *VendorMatcher
DefaultVendorMatcher returns a new matcher preloaded with the curated built-in patterns (see defaultVendorPatterns). The returned matcher is independent, so Add-ing to it does not affect the package-level IsVendored.
func NewVendorMatcher ¶ added in v0.6.0
func NewVendorMatcher() *VendorMatcher
NewVendorMatcher returns an empty matcher — it matches nothing until you Add patterns. Use it to opt out of the built-in set entirely.
func (*VendorMatcher) Add ¶ added in v0.6.0
func (m *VendorMatcher) Add(patterns ...string) error
Add compiles and appends regexp patterns, each matched against the slash-separated path. It returns the first compile error and adds nothing in that case.
func (*VendorMatcher) Match ¶ added in v0.6.0
func (m *VendorMatcher) Match(path string) bool
Match reports whether path matches any registered pattern. Paths are normalised to forward slashes before matching.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package celindicators provides the CEL backend for projectdetect's CELExpr indicators.
|
Package celindicators provides the CEL backend for projectdetect's CELExpr indicators. |