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.cr

Constant 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 / .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"}
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.json is not "any .json".

Instance Method Summary

Instance Method Detail

def add_android_source_prefixes_from_dir(dir : String, prefixes : Array(String)) #

[View source]
def android_source_file?(path : String, prefixes : Array(String)) : Bool #

[View source]
def android_source_prefixes_for_manifest(manifest_path : String) : Array(String) #

[View source]
def 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.


[View source]
def 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 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.


[View source]
def mobile_detector?(name : String) : Bool #

[View source]
def 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.


[View source]