module

Noir::Detection

Detection-pass internals: the ignore lists, the Android source-set resolver, and the applicable? memo that keeps the per-file walk off the O(detectors) path.

These were seven top-level defs and seven SCREAMING constants, each hand-prefixed detector_ / DETECTOR_ — a namespace spelled out by convention because there was no module to put them in. ANDROID_SOURCE_SUBDIRS and ANDROID_EMBEDDED_SERVER_MARKER did not even get the prefix, and were sitting in the global namespace under names generic enough to collide.

The two phase entry points (build_detector_list, detect_techs) stay top-level: they are the detection pass's public surface, called from NoirRunner and the specs.

Constants

ANDROID_EMBEDDED_SERVER_MARKER = /io\.ktor\.server\.|embeddedServer|\brouting\s*\{|\bfun\s+Route\.|org\.http4k\.routing|@RestController|@RequestMapping|@(?:Get|Post|Put|Delete|Patch)Mapping|\bRouterFunction\b/

Strong server-routing constructs. A .kt / .java file that sits inside an Android app's source set is normally scoped to mobile detectors only (an incidental import ...SpringApplication must not flag the project as a server). But an Android app can legitimately embed an on-device HTTP server — e.g. plain-app runs a local Ktor web server whose routes live under app/src/main/java/.... When a file carries one of these markers it is a real server, so the Ktor / http4k / Spring detectors are allowed to run on it. Kept as a single precompiled constant — recompiling it per file would recreate the PCRE2 program on every read.

ANDROID_SOURCE_SUBDIRS = Set {"aidl", "assets", "cpp", "java", "jni", "kotlin", "res"}
APP_MANIFESTS = BUILD_MANIFESTS - Set {"CMakeLists.txt", "meson.build", "Makefile"}

Language-app manifests: BUILD_MANIFESTS less the build files that are just as common beside C/C++ trees (a bare Makefile says nothing).

BUILD_MANIFESTS = Set {"package.json", "composer.json", "go.mod", "Gemfile", "pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "build.sbt", "build.xml", "Cargo.toml", "shard.yml", "mix.exs", "pubspec.yaml", "Package.swift", "setup.py", "setup.cfg", "pyproject.toml", "requirements.txt", "Pipfile", "deno.json", "deno.jsonc", "build.zig", "CMakeLists.txt", "meson.build", "Makefile", "project.clj", "deps.edn", "stack.yaml", "rebar.config", "gleam.toml", "cpanfile", "Makefile.PL", "dune-project"}
BUILD_OUTPUT_DIR_NAMES = Set {"dist", "build", "target", "out"}

The build-output subset, also pruned next to a src/ directory or a tsconfig.json: a Gradle subproject configured from the root subprojects {} block has no build file of its own, and a TypeScript package's manifest often sits one level above its stale dist/.

IGNORED_DIR_NAMES = Set {".git", ".idea", ".vscode", ".claude", "node_modules", "__pycache__", ".venv", "venv", ".uv-cache", ".pytest_cache", ".tox", ".gradle", ".bundle", ".dart_tool", ".cargo", ".terraform", ".yarn", ".zig-cache", "zig-cache", ".zig-out", "zig-out", ".cache", ".next", ".nuxt", ".svelte-kit", ".turbo", ".parcel-cache", ".serverless", ".expo", ".aws-sam", "cdk.out", ".vercel", ".netlify", ".output", ".wrangler", ".coverage", "Pods", "__MACOSX"}
IGNORED_DIR_SUFFIXES = Set {".xcassets"}
MANIFEST_SCOPED_DIR_NAMES = Set {"vendor", "dist", "build", "target", "out", "tmp", "coverage"}

Names that are build output or a dependency cache only when they sit next to the project manifest that produces them (vendor/ beside composer.json / go.mod / Gemfile, target/ beside pom.xml, build/ beside build.gradle). Anywhere else they are ordinary source directories — an Express routes/vendor/ module or a Java com.acme.build package — and pruning them by bare name dropped their endpoints with "errors": [].

MIX_SCOPED_DIR_NAMES = MANIFEST_SCOPED_DIR_NAMES + Set {"deps", "_build"}

Mix fetches every dependency's source into deps/ and compiles into _build/. Scanning them reports Plug/Phoenix's own docs and generator templates as routes, and their defmacro get/scope shadow the app's router calls. Only next to mix.exs: elsewhere deps/ is an ordinary directory name.

MOBILE_DETECTOR_NAMES = Set {"android", "ios", "well_known_applinks"}
NATIVE_BUILD_MANIFESTS = Set {"CMakeLists.txt", "meson.build", "BUILD.bazel", "WORKSPACE", "WORKSPACE.bazel", "MODULE.bazel", "BUILD.gn", "configure.ac", "SConstruct"}

Build files that make a tree C/C++-style source with vendored libraries, and weaker hints (submodules, Chromium's DEPS) that only count when no language manifest sits beside them.

NO_DIR_NAMES = Set(String).new
PATH_SEGMENT_PROBES = ["proj/grails-app/conf/application", "proj/supabase/migrations/001.sql", "proj/migrations/001.sql", "proj/supabase/config.toml", "proj/server/api/hello.js", "proj/server/routes/hello.js", "proj/pages/api/hello.js", "proj/app/api/hello/route.ts", "proj/routes/+server.ts", "proj/routes/index.dart", "proj/directus/snapshots/snap.json", "proj/wp-content/plugins/x.php", "proj/metadata/databases/tables.yaml", "proj/Magento/module.xml"]

Paths that embed directory segments real detectors gate on (/grails-app/, /migrations/, /server/api/, …). Used only to classify path-sensitive detectors — not as production file samples.

READ_WINDOW = 64

How far the walker may run ahead of the sequencer. Every pending file past the head holds its content in memory until the sequencer reaches it, and the content cache keeps most of them anyway.

SPECIAL_BASENAMES = Set {"package.json", "tsconfig.json", "composer.json", "composer.lock", "vercel.json", "now.json", "netlify.toml", "wrangler.toml", "Gemfile", "Gemfile.lock", "Package.swift", "Cargo.toml", "go.mod", "mix.exs", "pubspec.yaml", "pubspec.lock", "shard.yml", "shard.lock", "build.sbt", "pom.xml", "AndroidManifest.xml", "config.toml", "rebar.config", "erlang.mk", "project.clj", "deps.edn", "stack.yaml", "package.yaml", "gleam.toml", "manifest.toml", "paket.dependencies", "Caddyfile", "Dockerfile", "Makefile", "Rakefile", "serverless.yml", "serverless.yaml", "app.yaml", "openapi.yaml", "openapi.json", "swagger.json", "swagger.yaml"}

Filenames that detectors match by exact basename (often with path constraints like "must sit at the project root"). These must not share an extension-only cache bucket — e.g. vercel.json is not "any .json".

VENDORED_DIR_NAMES = Set {"third_party", "3rdparty"}

Vendored-library directories (Google/Chromium and CMake convention): a bundled Duktape debugger UI under third_party/ reads as the project's own Express routes. Only the underscore/digit-prefixed spellings: third-party/ and thirdparty name first-party integration code and URL segments.

The name alone is not evidence: a Flask app/third_party/routes.py, a Next.js pages/api/third_party/callback.ts or an Express routes/third_party/webhooks.js serves real routes. They are pruned only when the nearest manifest at or above their parent says the tree is native source (native_source_root?).

VENDORING_HINTS = Set {".gitmodules", "DEPS"}

Instance methods

add_android_source_prefixes_from_dir(dir : String, prefixes : Array(String))
Source
android_source_file?(path : String, prefixes : Array(String)) : Bool
Source
android_source_prefixes_for_manifest(manifest_path : String) : Array(String)
Source
build_applicable_lookup(detectors : Array(Detector)) : Proc(String, Array(Int32))

Build a lookup that turns a path into the list of detector indices whose applicable? returns true — without re-walking every detector on every file. Most detectors only inspect extension / basename, so their answers are memoized by basename. Detectors that look at path segments or root placement are classified as path-sensitive and always evaluated against the real path.

Source
dependency_vendor_dir?(path : String) : Bool

A Composer or go mod vendor tree, recognised by its own contents wherever it sits (a nested app whose composer.json lives elsewhere).

Source
ignored_dir_entry?(entry : String, full_path : String, dir_has_shard : Bool, scoped_names : Set(String), native_root : Bool) : Bool

Whether a directory entry names a subtree the walk prunes on purpose (dependency cache, build output next to its manifest, Crystal's lib/ next to a shard.yml).

Extracted from the directory branch of the walk so the symlink branch can ask the same question: a symlink is not a directory to File.info? with follow_symlinks: false, so a node_modules or .venv symlink lands there instead — and reporting that as lost coverage would fire on every pnpm workspace, which is the opposite of the point.

scoped_names is what scoped_dir_names returned for the parent; native_root is what native_source_root? returned for it.

Source
mobile_detector?(name : String) : Bool
Source
native_source_root?(entries : Array(Noir::DirListing::Entry), inherited : Bool) : Bool

Whether third_party/ and 3rdparty/ children of a directory are vendored libraries: its listing names a native build file (true), a language manifest with no native build file beside it (false), or inherited (the nearest ancestor's answer) when it names neither. .gitmodules / DEPS count as native only without a language manifest, so a first-party app that merely uses submodules keeps its third_party/ routes.

Source
path_sensitive?(detector : Detector) : Bool

Whether applicable? depends on more than the basename (root placement, directory segments, multi-hop path layout). Those detectors must always see the real path in the hot loop.

Source
scoped_dir_names(entries : Array(Noir::DirListing::Entry)) : Set(String)

Which MANIFEST_SCOPED_DIR_NAMES a directory's listing licenses the walk to prune among its children.

Source

Nested types