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
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.
Language-app manifests: BUILD_MANIFESTS less the build files that are
just as common beside C/C++ trees (a bare Makefile says nothing).
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/.
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 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.
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.
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.
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.
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-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?).
Instance methods
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.
A Composer or go mod vendor tree, recognised by its own contents
wherever it sits (a nested app whose composer.json lives elsewhere).
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.
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.
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.
Which MANIFEST_SCOPED_DIR_NAMES a directory's listing licenses the
walk to prune among its children.
True when the symlink at path resolves inside one of the scan bases
(real_bases are realpaths), so the walk covers its target anyway.
A dangling link, or one that leaves every base, is lost coverage — and
so is one into a directory the walk may prune (vendor/, deps/,
node_modules/): over-reporting a link there beats hiding a lost tree.