module
Noir::Detection
Overview
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.
Extended Modules
Defined in:
detector/detector.crConstant Summary
-
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/.javafile that sits inside an Android app's source set is normally scoped to mobile detectors only (an incidentalimport ...SpringApplicationmust 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 underapp/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"} -
IGNORED_DIR_NAMES =
Set {".git", ".idea", ".vscode", ".claude", "node_modules", "vendor", "__pycache__", ".venv", "venv", ".pytest_cache", ".tox", ".gradle", ".bundle", ".dart_tool", ".cargo", ".terraform", ".zig-cache", "zig-cache", ".zig-out", "zig-out", "dist", "build", "target", "out", "tmp", ".cache", ".next", ".nuxt", ".svelte-kit", ".turbo", ".parcel-cache", ".serverless", ".expo", "coverage", ".coverage", "Pods", "__MACOSX"} -
IGNORED_DIR_SUFFIXES =
Set {".xcassets"} -
MOBILE_DETECTOR_NAMES =
Set {"android", "ios", "well_known_applinks"} -
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. -
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.jsonis not "any .json".
Instance Method Summary
- #add_android_source_prefixes_from_dir(dir : String, prefixes : Array(String))
- #android_source_file?(path : String, prefixes : Array(String)) : Bool
- #android_source_prefixes_for_manifest(manifest_path : String) : Array(String)
-
#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. -
#ignored_dir_entry?(entry : String, dir_has_shard : Bool) : Bool
Whether a directory entry names a subtree the walk prunes on purpose (dependency cache, build output, Crystal's
lib/next to ashard.yml). - #mobile_detector?(name : String) : Bool
-
#path_sensitive?(detector : Detector) : Bool
Whether
applicable?depends on more than the basename (root placement, directory segments, multi-hop path layout).
Instance Method Detail
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.
Whether a directory entry names a subtree the walk prunes on purpose
(dependency cache, build output, 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.
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.