diff --git a/README.md b/README.md
index bf674d9..fa0e456 100644
--- a/README.md
+++ b/README.md
@@ -288,23 +288,23 @@ Separately from resources, brief reports agent skills the project provides. Thes
## What it detects
-54 language ecosystems with 547 tool definitions across 22 categories.
+Language ecosystems and development tools across multiple categories.
**Languages:** Ada, C, C#, C++, COBOL, Clojure, Common Lisp, Crystal, D, Dart, Deno, Elixir, Elm, Emacs Lisp, Erlang, F#, Fortran, GDScript, Gleam, Go, Groovy, Haskell, Haxe, Java, JavaScript, Julia, Kotlin, Lua, Mojo, Nim, Nix, OCaml, Objective-C, Odin, PHP, Perl, Prolog, Python, R, Racket, Roc, Ruby, Rust, Scala, Scheme, Solidity, Swift, Tcl, TypeScript, V, VHDL, Verilog, Zig.
-**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Maven, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, sbt, setuptools, uv, vcpkg.
+**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Maven, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, renv, sbt, setuptools, uv, vcpkg.
-**Test:** AVA, Alcotest, Artillery, Bats, Bruno, Catch2, Cucumber, Cypress, EUnit, ExUnit, Gatling, Ginkgo, Google Test, Hspec, Hurl, JUnit, Jasmine, Jest, Kotest, Lighthouse CI, Locust, MSW, Minitest, Mocha, Newman, PHPUnit, Pest, Playwright, REST Client, RSpec, ScalaTest, Selenium, Testify, Testing Library, Vitest, XCTest, axe-core, benchmark-ips, cargo test, clojure.test, criterion, crystal spec, dart test, deno test, dotnet test, gleam test, go test, hyperfine, k6, kotlin.test, pytest, pytest-benchmark, tape, testament, zig test.
+**Test:** ASV, AVA, Alcotest, Artillery, Bats, BenchmarkTools.jl, Bruno, Catch2, Cucumber, Cypress, EUnit, ExUnit, Gatling, Ginkgo, Google Test, Hspec, Hurl, JUnit, Jasmine, Jest, Kotest, Lighthouse CI, Locust, MSW, Minitest, Mocha, Newman, PHPUnit, Pest, Playwright, REST Client, RSpec, ScalaTest, Selenium, Testify, Testing Library, Vitest, XCTest, axe-core, benchmark-ips, cargo test, clojure.test, criterion, crystal spec, dart test, deno test, dotnet test, gleam test, go test, hyperfine, k6, kotlin.test, nf-test, pytest, pytest-benchmark, tape, testament, testthat, tinytest, tox, vdiffr, zig test.
-**Lint:** Ameba, Biome, Checkstyle, Clippy, Credo, ESLint, Flake8, HLint, Husky, Lefthook, Overcommit, PHP_CodeSniffer, PMD, Pylint, Revive, Roslyn Analyzers, RuboCop, Ruff, ShellCheck, SpotBugs, Stylelint, SwiftLint, Vale, WartRemover, actionlint, clang-tidy, clj-kondo, commitlint, cspell, dart analyze, deno lint, detekt, elvis, golangci-lint, hadolint, markdownlint, oxlint, pre-commit, typos.
+**Lint:** Ameba, Biome, Checkstyle, Clippy, Credo, ESLint, Flake8, Fortitude, HLint, Husky, Lefthook, Overcommit, PHP_CodeSniffer, PMD, Pylint, Revive, Roslyn Analyzers, RuboCop, Ruff, ShellCheck, SpotBugs, Stylelint, SwiftLint, Vale, WartRemover, actionlint, clang-tidy, clj-kondo, commitlint, cspell, dart analyze, deno lint, detekt, elvis, golangci-lint, hadolint, lintr, markdownlint, oxlint, pre-commit, typos.
-**Format:** Black, Ormolu, PHP CS Fixer, Prettier, Spotless, StandardRB, SwiftFormat, clang-format, cljfmt, crystal tool format, dart format, deno fmt, dotnet format, dprint, erlfmt, gleam format, gofmt, google-java-format, isort, ktlint, mix format, nimpretty, ocamlformat, rustfmt, scalafmt, yapf, zig fmt.
+**Format:** Black, JuliaFormatter, Ormolu, PHP CS Fixer, Prettier, Runic, Spotless, StandardRB, SwiftFormat, clang-format, cljfmt, crystal tool format, dart format, deno fmt, dotnet format, dprint, erlfmt, gleam format, gofmt, google-java-format, isort, ktlint, mix format, nimpretty, ocamlformat, rustfmt, scalafmt, styler, yapf, zig fmt.
**Typecheck:** Dialyxir, Dialyzer, Flow, PHPStan, Pyright, Sorbet, Steep, mypy, tsc.
-**Docs:** Docsify, Docusaurus, Dokka, Doxygen, ExDoc, Hugo, Javadoc, Jekyll, MkDocs, Nextra, Read the Docs, Redoc, Sphinx, Starlight, Storybook, Swagger UI, TypeDoc, VitePress, Yard, Zensical, cargo doc, dart doc, deno doc, mdBook, phpDocumentor, pkgsite.
+**Docs:** Docsify, Documenter.jl, Docusaurus, Dokka, Doxygen, ExDoc, Hugo, Javadoc, Jekyll, MkDocs, MultiQC, MyST-Parser, Nextra, Quarto, R Markdown, Read the Docs, Redoc, Sphinx, Starlight, Storybook, Swagger UI, TypeDoc, VitePress, Yard, Zensical, cargo doc, dart doc, deno doc, knitr, mdBook, phpDocumentor, pkgdown, pkgsite, roxygen2.
-**Build:** Actix Web, AdonisJS, Angular, Astro, Autotools, Axum, CMake, Django, Dune, Echo, Electron, Eleventy, Ember.js, Express, FastAPI, Fastify, Fiber, Flask, Flutter, Foundry, Gatsby, Gin, GoReleaser, Hardhat, Hono, Invoke, Just, Koa, Laravel, Less, Mage, Make, Meson, NestJS, Next.js, Nuxt, Parcel, Phoenix, PostCSS, Qwik, Rails, Rake, React Native, Remix, Rocket, Rollup, Rspack, SWC, Sass, Sinatra, Spin, Spring Boot, Svelte, SvelteKit, Symfony, Task, Tauri, Vite, Vue, Webpack, cross, esbuild, tsup.
+**Build:** Actix Web, AdonisJS, Angular, Astro, Autotools, Axum, CMake, Django, Dune, Echo, Electron, Eleventy, Ember.js, Express, FastAPI, Fastify, Fiber, Flask, Flutter, Foundry, Gatsby, Gin, GoReleaser, Hardhat, Hono, Invoke, Just, Koa, Laravel, Less, Mage, Make, Meson, NestJS, Next.js, Nextflow, Nuxt, Parcel, Phoenix, PostCSS, Qwik, Rails, Rake, React Native, Remix, Rocket, Rollup, Rspack, SWC, Sass, Sinatra, Snakemake, Spin, Spring Boot, Svelte, SvelteKit, Symfony, Task, Tauri, Vite, Vue, Webpack, cibuildwheel, cross, esbuild, nf-core, targets, tsup.
**Native Ext:** Maturin, Neon, Rustler, meson-python, mkmf, napi-rs, node-gyp, phpize, rb-sys, setuptools Extension, setuptools-rust.
@@ -320,17 +320,17 @@ Separately from resources, brief reports agent skills the project provides. Thes
**Container:** Cloud Native Buildpacks, Dev Container, Docker, Docker Compose, Podman.
-**Infra:** AWS CDK, Ansible, CloudFormation, Helm, Kubernetes, Kustomize, Packer, Pulumi, Serverless Framework, Terraform, Vagrant.
+**Infra:** AWS CDK, Ansible, CloudFormation, DVC, Dockstore, Helm, Kubernetes, Kustomize, Packer, Pulumi, Serverless Framework, Terraform, Vagrant.
**Monorepo:** Bazel, Cargo workspaces, Go workspace, Lerna, Moon, Nx, Pants, Rush, Turborepo, Yarn workspaces, pnpm workspaces.
-**Environment:** Flipper, JetBrains IDE, LaunchDarkly, Mise, Pixi, Unleash, VS Code, Volta, asdf, direnv, dotenv, pyenv.
+**Environment:** Flipper, JetBrains IDE, Jupyter, LaunchDarkly, Mise, Pixi, Unleash, VS Code, Volta, asdf, direnv, dotenv, pyenv.
**i18n:** Crowdin, Fluent, FormatJS, Lingui, Rails i18n, Transifex, gettext, i18next, vue-i18n.
**Release:** Changesets, cargo-release, conventional-changelog, git-cliff, np, release-please, semantic-release, standard-version, twine.
-**Coverage:** Codecov, Coveralls, Excoveralls, JaCoCo, Sentry, SimpleCov, c8, cargo-tarpaulin, coverage.py, go test -cover, nyc.
+**Coverage:** Codecov, Coveralls, Excoveralls, JaCoCo, Sentry, SimpleCov, c8, cargo-tarpaulin, coverage.py, covr, go test -cover, nyc.
**Dep Updates:** Dependabot, Git Submodules, Renovate.
diff --git a/cmd/brief/list_test.go b/cmd/brief/list_test.go
new file mode 100644
index 0000000..73d7969
--- /dev/null
+++ b/cmd/brief/list_test.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/git-pkgs/brief"
+ "github.com/git-pkgs/brief/kb"
+)
+
+func TestWriteToolsReadmeOmitsKnowledgeBaseTotals(t *testing.T) {
+ var out strings.Builder
+ writeToolsReadme(&out, &kb.KnowledgeBase{})
+
+ const introduction = "Language ecosystems and development tools across multiple categories."
+ if !strings.Contains(out.String(), introduction) {
+ t.Errorf("README output does not contain count-free introduction %q", introduction)
+ }
+ if strings.Contains(out.String(), "language ecosystems with") {
+ t.Errorf("README output contains numeric knowledge-base totals: %q", out.String())
+ }
+}
+
+func TestToolsReadmeMatchesGeneratedOutput(t *testing.T) {
+ knowledgeBase, err := kb.Load(brief.KnowledgeFS)
+ if err != nil {
+ t.Fatalf("load knowledge base: %v", err)
+ }
+ var generated strings.Builder
+ writeToolsReadme(&generated, knowledgeBase)
+
+ readme, err := os.ReadFile("../../README.md")
+ if err != nil {
+ t.Fatalf("read README: %v", err)
+ }
+ readmeText := strings.ReplaceAll(string(readme), "\r\n", "\n")
+ const start = "\n"
+ const end = ""
+ _, afterStart, ok := strings.Cut(readmeText, start)
+ if !ok {
+ t.Fatal("README tools start marker is missing")
+ }
+ section, _, ok := strings.Cut(afterStart, end)
+ if !ok {
+ t.Fatal("README tools end marker is missing")
+ }
+ if section != generated.String() {
+ t.Error("README tools block differs from `brief list -readme tools` output")
+ }
+}
diff --git a/cmd/brief/main.go b/cmd/brief/main.go
index 875db85..1ffea09 100644
--- a/cmd/brief/main.go
+++ b/cmd/brief/main.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
+ "io"
"os"
"reflect"
"runtime/debug"
@@ -232,6 +233,10 @@ func listTools(knowledgeBase *kb.KnowledgeBase) {
}
func listToolsReadme(knowledgeBase *kb.KnowledgeBase) {
+ writeToolsReadme(os.Stdout, knowledgeBase)
+}
+
+func writeToolsReadme(out io.Writer, knowledgeBase *kb.KnowledgeBase) {
// Group tools by category, deduplicating names.
seen := make(map[string]map[string]bool)
byCategory := make(map[string][]string)
@@ -255,24 +260,17 @@ func listToolsReadme(knowledgeBase *kb.KnowledgeBase) {
languages := byCategory["language"]
- ecosystems := knowledgeBase.AllEcosystems()
- totalTools := 0
- for _, names := range byCategory {
- totalTools += len(names)
- }
-
- _, _ = fmt.Fprintf(os.Stdout, "## What it detects\n\n")
- _, _ = fmt.Fprintf(os.Stdout, "%d language ecosystems with %d tool definitions across %d categories.\n\n",
- len(ecosystems), totalTools, len(byCategory))
+ _, _ = fmt.Fprint(out, "## What it detects\n\n")
+ _, _ = fmt.Fprint(out, "Language ecosystems and development tools across multiple categories.\n\n")
// Languages
if len(languages) > 0 {
- _, _ = fmt.Fprintf(os.Stdout, "**Languages:** %s.\n\n", strings.Join(languages, ", "))
+ _, _ = fmt.Fprintf(out, "**Languages:** %s.\n\n", strings.Join(languages, ", "))
}
// Package managers
if pms := byCategory["package_manager"]; len(pms) > 0 {
- _, _ = fmt.Fprintf(os.Stdout, "**Package Managers:** %s.\n\n", strings.Join(pms, ", "))
+ _, _ = fmt.Fprintf(out, "**Package Managers:** %s.\n\n", strings.Join(pms, ", "))
}
// Tool categories in display order.
@@ -285,10 +283,10 @@ func listToolsReadme(knowledgeBase *kb.KnowledgeBase) {
if label == "" {
label = cat
}
- _, _ = fmt.Fprintf(os.Stdout, "**%s:** %s.\n\n", label, strings.Join(names, ", "))
+ _, _ = fmt.Fprintf(out, "**%s:** %s.\n\n", label, strings.Join(names, ", "))
}
- _, _ = fmt.Fprintf(os.Stdout, "Run `brief list tools` for the full list.\n")
+ _, _ = fmt.Fprint(out, "Run `brief list tools` for the full list.\n")
}
func listEcosystems(knowledgeBase *kb.KnowledgeBase) {
diff --git a/detect/detect.go b/detect/detect.go
index bbf990d..811fe7a 100644
--- a/detect/detect.go
+++ b/detect/detect.go
@@ -473,6 +473,12 @@ func (e *Engine) detectCategory(category string) []brief.Detection {
// matchTool checks if a tool definition matches the project.
// Returns the confidence level, or empty string if no match.
func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence {
+ for _, pattern := range tool.Detect.ExcludeFiles {
+ if e.exists(pattern) {
+ return ""
+ }
+ }
+
best := brief.Confidence("")
for _, pattern := range tool.Detect.Files {
@@ -488,7 +494,7 @@ func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence {
}
for file, patterns := range tool.Detect.FileContains {
- if e.contains(file, patterns) {
+ if e.contentSignalMatches(file, patterns, tool.Detect.ExcludeFileContains[file]) {
best = brief.ConfidenceHigh
}
}
@@ -511,6 +517,10 @@ func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence {
return best
}
+func (e *Engine) contentSignalMatches(file string, patterns, excluded []string) bool {
+ return e.contains(file, patterns) && (len(excluded) == 0 || !e.contains(file, excluded))
+}
+
// exists checks if a file, directory, or glob pattern matches something at the project root.
// A trailing "/" means the pattern must match a directory. Glob patterns without
// a trailing "/" only match regular files so that a NEWS.d/ directory does not
diff --git a/detect/detect_test.go b/detect/detect_test.go
index 7ba5e09..97cb067 100644
--- a/detect/detect_test.go
+++ b/detect/detect_test.go
@@ -1257,6 +1257,194 @@ func TestGradleJavaKotlinDSL(t *testing.T) {
}
}
+func TestExpandedToolFormatDetection(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ content string
+ extraFiles map[string]string
+ category string
+ tool string
+ notTool string
+ }{
+ {
+ name: "Gradle version catalog",
+ path: "gradle/libs.versions.toml",
+ content: "[versions]\n",
+ extraFiles: map[string]string{"Main.java": "class Main {}\n"},
+ category: "package_manager",
+ tool: "Gradle",
+ },
+ {
+ name: "NuGet central packages",
+ path: "Directory.Packages.props",
+ content: "\n",
+ extraFiles: map[string]string{"Program.cs": "class Program {}\n"},
+ category: "package_manager",
+ tool: "NuGet",
+ },
+ {
+ name: "NuGet shared build properties",
+ path: "Directory.Build.props",
+ content: "\n",
+ extraFiles: map[string]string{"Program.cs": "class Program {}\n"},
+ category: "package_manager",
+ tool: "NuGet",
+ },
+ {
+ name: "Helm chart lock",
+ path: "Chart.lock",
+ content: "dependencies: []\n",
+ category: "infrastructure",
+ tool: "Helm",
+ },
+ {
+ name: "Helm legacy requirements",
+ path: "requirements.yaml",
+ content: "dependencies:\n - name: example\n version: 1.0.0\n",
+ category: "infrastructure",
+ tool: "Helm",
+ notTool: "Ansible",
+ },
+ {
+ name: "Helm legacy requirements lock",
+ path: "requirements.lock",
+ content: "dependencies: []\n",
+ category: "infrastructure",
+ tool: "Helm",
+ },
+ {
+ name: "Yarn Plug'n'Play",
+ path: ".pnp.cjs",
+ content: "module.exports = {};\n",
+ extraFiles: map[string]string{"package.json": "{}\n"},
+ category: "package_manager",
+ tool: "Yarn",
+ },
+ {
+ name: "legacy pnpm lockfile",
+ path: "shrinkwrap.yaml",
+ content: "lockfileVersion: 3\n",
+ extraFiles: map[string]string{"package.json": "{}\n"},
+ category: "package_manager",
+ tool: "pnpm",
+ },
+ {
+ name: "Ansible Galaxy requirements yml",
+ path: "requirements.yml",
+ content: "roles: []\n",
+ category: "infrastructure",
+ tool: "Ansible",
+ notTool: "Helm",
+ },
+ {
+ name: "Ansible Galaxy requirements yaml",
+ path: "requirements.yaml",
+ content: "roles: []\n",
+ category: "infrastructure",
+ tool: "Ansible",
+ notTool: "Helm",
+ },
+ {
+ name: "Ansible Galaxy named role requirement",
+ path: "requirements.yml",
+ content: "- name: geerlingguy.apache\n",
+ category: "infrastructure",
+ tool: "Ansible",
+ notTool: "Helm",
+ },
+ {
+ name: "Ansible Galaxy included role requirements",
+ path: "requirements.yaml",
+ content: "- include: requirements/common.yml\n",
+ category: "infrastructure",
+ tool: "Ansible",
+ notTool: "Helm",
+ },
+ {
+ name: "Ansible Galaxy metadata yml",
+ path: "galaxy.yml",
+ content: "namespace: example\n",
+ category: "infrastructure",
+ tool: "Ansible",
+ },
+ {
+ name: "Ansible Galaxy metadata yaml",
+ path: "galaxy.yaml",
+ content: "namespace: example\n",
+ category: "infrastructure",
+ tool: "Ansible",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ dir := t.TempDir()
+ writeFile(t, dir, test.path, test.content)
+ for path, content := range test.extraFiles {
+ writeFile(t, dir, path, content)
+ }
+
+ r, err := New(loadKB(t), dir).Run()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if test.category == "package_manager" {
+ if !slices.Contains(packageManagerNames(r), test.tool) {
+ t.Errorf("expected %s package manager, got %v", test.tool, packageManagerNames(r))
+ }
+ return
+ }
+ assertToolDetected(t, r, test.category, test.tool)
+ if test.notTool != "" {
+ assertToolNotDetected(t, r, test.category, test.notTool)
+ }
+ })
+ }
+}
+
+func TestBazelBuildDoesNotConflictWithPants(t *testing.T) {
+ t.Run("Python Bazel project", func(t *testing.T) {
+ dir := t.TempDir()
+ writeFile(t, dir, "BUILD", "py_library(name = \"example\")\n")
+ writeFile(t, dir, "example.py", "VALUE = 1\n")
+
+ r, err := New(loadKB(t), dir).Run()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ assertToolDetected(t, r, "monorepo", "Bazel")
+ assertToolNotDetected(t, r, "monorepo", "Pants")
+ })
+
+ t.Run("Pants project", func(t *testing.T) {
+ dir := t.TempDir()
+ writeFile(t, dir, "pants.toml", "[GLOBAL]\n")
+ writeFile(t, dir, "BUILD", "python_sources()\n")
+ writeFile(t, dir, "example.py", "VALUE = 1\n")
+
+ r, err := New(loadKB(t), dir).Run()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ assertToolDetected(t, r, "monorepo", "Pants")
+ assertToolNotDetected(t, r, "monorepo", "Bazel")
+ })
+}
+
+func TestHelmRequirementsDoesNotHideAnsibleConfig(t *testing.T) {
+ dir := t.TempDir()
+ writeFile(t, dir, "requirements.yaml", "dependencies:\n - name: example\n version: 1.0.0\n")
+ writeFile(t, dir, "ansible.cfg", "[defaults]\n")
+
+ r, err := New(loadKB(t), dir).Run()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ assertToolDetected(t, r, "infrastructure", "Helm")
+ assertToolDetected(t, r, "infrastructure", "Ansible")
+}
+
func TestGradleJavaGroovyDSL(t *testing.T) {
// Regression for #84: with build.gradle in app/ and source under
// app/src/main/java//, both Java and Gradle must be detected.
@@ -2019,6 +2207,16 @@ func assertToolDetected(t *testing.T, r *brief.Report, category, name string) {
t.Errorf("expected %s in %s category", name, category)
}
+func assertToolNotDetected(t *testing.T, r *brief.Report, category, name string) {
+ t.Helper()
+ for _, tool := range r.Tools[category] {
+ if tool.Name == name {
+ t.Errorf("did not expect %s in %s category", name, category)
+ return
+ }
+ }
+}
+
func assertHighConfidenceToolDetected(
t *testing.T,
r *brief.Report,
diff --git a/detect/filter.go b/detect/filter.go
index e695379..c250d6e 100644
--- a/detect/filter.go
+++ b/detect/filter.go
@@ -268,8 +268,8 @@ func (fc *filterContext) filterPlatforms(plat *brief.PlatformInfo, changedFiles
}
// toolMatchesChangedFiles checks whether any changed file is relevant to a tool's
-// detection signals: config files, lockfile, detection file patterns, or
-// file_contains targets.
+// detection signals: config files, lockfile, detection or exclusion file
+// patterns, or file_contains targets.
func toolMatchesChangedFiles(tool *kb.ToolDef, changed map[string]bool, changedExts map[string]bool) bool {
if matchesConfigFiles(tool, changed) {
return true
@@ -290,7 +290,12 @@ func matchesConfigFiles(tool *kb.ToolDef, changed map[string]bool) bool {
}
func matchesDetectionPatterns(tool *kb.ToolDef, changed map[string]bool, changedExts map[string]bool) bool {
- for _, pattern := range tool.Detect.Files {
+ return matchesPathPatterns(tool.Detect.Files, changed, changedExts) ||
+ matchesPathPatterns(tool.Detect.ExcludeFiles, changed, changedExts)
+}
+
+func matchesPathPatterns(patterns []string, changed map[string]bool, changedExts map[string]bool) bool {
+ for _, pattern := range patterns {
if changed[pattern] {
return true
}
@@ -319,7 +324,20 @@ func matchesDetectionPatterns(tool *kb.ToolDef, changed map[string]bool, changed
}
func matchesContentTargets(tool *kb.ToolDef, changed map[string]bool) bool {
- for pattern := range tool.Detect.FileContains {
+ if matchesContentPatterns(tool.Detect.FileContains, changed) ||
+ matchesContentPatterns(tool.Detect.ExcludeFileContains, changed) {
+ return true
+ }
+ for file := range tool.Detect.KeyExists {
+ if changed[file] {
+ return true
+ }
+ }
+ return false
+}
+
+func matchesContentPatterns(patterns map[string][]string, changed map[string]bool) bool {
+ for pattern := range patterns {
if changed[pattern] {
return true
}
@@ -331,10 +349,5 @@ func matchesContentTargets(tool *kb.ToolDef, changed map[string]bool) bool {
}
}
}
- for file := range tool.Detect.KeyExists {
- if changed[file] {
- return true
- }
- }
return false
}
diff --git a/detect/filter_test.go b/detect/filter_test.go
index d85e177..8079369 100644
--- a/detect/filter_test.go
+++ b/detect/filter_test.go
@@ -196,6 +196,37 @@ func TestToolMatchesChangedFiles_FileContainsGlob(t *testing.T) {
}
}
+func TestToolMatchesChangedFiles_ExcludeFileContains(t *testing.T) {
+ tool := &kb.ToolDef{
+ Detect: kb.DetectInfo{
+ Files: []string{"BUILD"},
+ ExcludeFileContains: map[string][]string{
+ "requirements.yaml": {"dependencies:"},
+ },
+ },
+ }
+ changed := map[string]bool{"requirements.yaml": true}
+ if !toolMatchesChangedFiles(tool, changed, nil) {
+ t.Error("expected exclusion content target to match changed file")
+ }
+}
+
+func TestFilterByChangedFiles_ExcludeFileRemoved(t *testing.T) {
+ dir := t.TempDir()
+ writeFile(t, dir, "BUILD", "py_library(name = \"example\")\n")
+ writeFile(t, dir, "example.py", "VALUE = 1\n")
+
+ knowledgeBase := loadKB(t)
+ r, err := New(knowledgeBase, dir).Run()
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ assertToolDetected(t, r, "monorepo", "Bazel")
+
+ filtered := FilterByChangedFiles(r, knowledgeBase, []string{"pants.toml"})
+ assertToolDetected(t, filtered, "monorepo", "Bazel")
+}
+
func TestFilterByChangedFiles_PackageManagers(t *testing.T) {
knowledgeBase := loadKB(t)
diff --git a/kb/kb.go b/kb/kb.go
index 345282a..6b98178 100644
--- a/kb/kb.go
+++ b/kb/kb.go
@@ -35,12 +35,14 @@ type ToolInfo struct {
// DetectInfo holds the detection primitives for a tool.
type DetectInfo struct {
- Files []string `toml:"files"`
- Dependencies []string `toml:"dependencies"`
- DevDependencies []string `toml:"dev_dependencies"`
- FileContains map[string][]string `toml:"file_contains"`
- KeyExists map[string][]string `toml:"key_exists"`
- Ecosystems []string `toml:"ecosystems"`
+ Files []string `toml:"files"`
+ ExcludeFiles []string `toml:"exclude_files"`
+ Dependencies []string `toml:"dependencies"`
+ DevDependencies []string `toml:"dev_dependencies"`
+ FileContains map[string][]string `toml:"file_contains"`
+ ExcludeFileContains map[string][]string `toml:"exclude_file_contains"`
+ KeyExists map[string][]string `toml:"key_exists"`
+ Ecosystems []string `toml:"ecosystems"`
}
// CommandInfo holds the commands associated with a tool.
diff --git a/knowledge/_shared/ansible.toml b/knowledge/_shared/ansible.toml
index 2cf969d..24637d8 100644
--- a/knowledge/_shared/ansible.toml
+++ b/knowledge/_shared/ansible.toml
@@ -7,7 +7,15 @@ repo = "https://github.com/ansible/ansible"
description = "IT automation and configuration management"
[detect]
-files = ["ansible.cfg", "playbooks/", "roles/", "inventory/"]
+files = ["ansible.cfg", "galaxy.yml", "galaxy.yaml", "playbooks/", "roles/", "inventory/"]
+
+[detect.file_contains]
+"requirements.yml" = ["roles:", "collections:", "- src:", "- name:", "- include:"]
+"requirements.yaml" = ["roles:", "collections:", "- src:", "- name:", "- include:"]
+
+[detect.exclude_file_contains]
+"requirements.yml" = ["dependencies:"]
+"requirements.yaml" = ["dependencies:"]
[commands]
run = "ansible-playbook"
diff --git a/knowledge/_shared/bazel.toml b/knowledge/_shared/bazel.toml
index d01ece0..f242b06 100644
--- a/knowledge/_shared/bazel.toml
+++ b/knowledge/_shared/bazel.toml
@@ -7,7 +7,8 @@ repo = "https://github.com/bazelbuild/bazel"
description = "Build and test tool for multi-language monorepos"
[detect]
-files = ["WORKSPACE", "WORKSPACE.bazel", "MODULE.bazel", "BUILD.bazel"]
+files = ["WORKSPACE", "WORKSPACE.bazel", "MODULE.bazel", "BUILD", "BUILD.bazel"]
+exclude_files = ["pants.toml"]
[commands]
run = "bazel build //..."
diff --git a/knowledge/_shared/helm.toml b/knowledge/_shared/helm.toml
index a2005ec..4582472 100644
--- a/knowledge/_shared/helm.toml
+++ b/knowledge/_shared/helm.toml
@@ -7,7 +7,10 @@ repo = "https://github.com/helm/helm"
description = "Kubernetes package manager"
[detect]
-files = ["Chart.yaml", "Chart.yml", "charts/"]
+files = ["Chart.yaml", "Chart.yml", "Chart.lock", "requirements.lock", "charts/"]
+
+[detect.file_contains]
+"requirements.yaml" = ["dependencies:"]
[commands]
run = "helm install"
diff --git a/knowledge/csharp/nuget.toml b/knowledge/csharp/nuget.toml
index 61382be..4f7fe72 100644
--- a/knowledge/csharp/nuget.toml
+++ b/knowledge/csharp/nuget.toml
@@ -6,7 +6,7 @@ docs = "https://learn.microsoft.com/en-us/nuget/"
description = ".NET package manager"
[detect]
-files = ["*.csproj", "packages.config", "nuget.config"]
+files = ["*.csproj", "packages.config", "nuget.config", "Directory.Packages.props", "Directory.Build.props"]
ecosystems = ["csharp"]
[commands]
diff --git a/knowledge/java/gradle.toml b/knowledge/java/gradle.toml
index 5dfa7c3..f53958a 100644
--- a/knowledge/java/gradle.toml
+++ b/knowledge/java/gradle.toml
@@ -7,7 +7,7 @@ repo = "https://github.com/gradle/gradle"
description = "Build automation tool for Java and other JVM languages"
[detect]
-files = ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"]
+files = ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "gradle/libs.versions.toml"]
ecosystems = ["java", "groovy", "kotlin"]
[commands]
diff --git a/knowledge/node/pnpm.toml b/knowledge/node/pnpm.toml
index a3fa077..6b2dc81 100644
--- a/knowledge/node/pnpm.toml
+++ b/knowledge/node/pnpm.toml
@@ -7,7 +7,7 @@ repo = "https://github.com/pnpm/pnpm"
description = "Fast, disk space efficient package manager"
[detect]
-files = ["pnpm-lock.yaml"]
+files = ["pnpm-lock.yaml", "shrinkwrap.yaml"]
ecosystems = ["node"]
[commands]
diff --git a/knowledge/node/yarn.toml b/knowledge/node/yarn.toml
index 6f38f43..ec89775 100644
--- a/knowledge/node/yarn.toml
+++ b/knowledge/node/yarn.toml
@@ -7,7 +7,7 @@ repo = "https://github.com/yarnpkg/berry"
description = "Node.js package manager"
[detect]
-files = ["yarn.lock", ".yarnrc.yml", ".yarnrc"]
+files = ["yarn.lock", ".yarnrc.yml", ".yarnrc", ".pnp.cjs"]
ecosystems = ["node"]
[commands]
diff --git a/knowledge/python/pants.toml b/knowledge/python/pants.toml
index 9d3d70a..4bf873e 100644
--- a/knowledge/python/pants.toml
+++ b/knowledge/python/pants.toml
@@ -7,7 +7,7 @@ repo = "https://github.com/pantsbuild/pants"
description = "Build system for Python and polyglot monorepos"
[detect]
-files = ["pants.toml", "BUILD"]
+files = ["pants.toml"]
ecosystems = ["python"]
[commands]