class

CodeLocator

Inherits Reference < Object

Constants

DEFAULT_CONTENT_CACHE_BUDGET = (512_i64 * 1024) * 1024

Default content cache budget (bytes). Override via NOIR_CONTENT_CACHE_MAX_MB (value in megabytes). Set to 0 or the env NOIR_CONTENT_CACHE_DISABLE=true to disable caching entirely, in which case content_for always returns nil and analyzers fall through to File.read.

MAX_CONTENT_CACHE_MB = Int64::MAX // (1024_i64 * 1024)

Megabyte figures at or above this overflow Int64 once scaled to bytes. Crystal checks integer arithmetic, so the multiply below raised rather than wrapping: NOIR_CONTENT_CACHE_MAX_MB=99999999999999 aborted the whole scan with an unhandled OverflowError stack trace before the first file was read. Saturate instead — a budget that large already means "cache everything", which is exactly what Int64::MAX gives.

Constructors

instance
Source

Instance methods

all_files

Every registered path, in registration order.

Source
base_relative(path : String) : String

path relative to the scan base that owns it, /-separated and rooted with a leading /. With no registered bases (library callers, unit specs that drive a parser directly) the path is returned unchanged, which is exactly the pre-registry behaviour.

Source
build_basename_index

Build a basename => paths index from file_map.

The companion to build_extension_index, for the "find the file(s) whose path ends with a/b/c.py" lookups that analyzers otherwise answer with Dir.glob("root/**/a/b/c.py"). That glob walks the whole tree from the scan root — descending into node_modules, .git and every other subtree the detector deliberately pruned — and costs one opendir/getdirentries pass per call. Django's ROOT_URLCONF resolution ran it once per settings.py, which on a 44k-file monorepo was ~73% of the entire analysis phase.

Keyed on basename because that is the selective part of those patterns: urls.py narrows 44k files to a handful, and the caller confirms the rest of the path with a cheap ends_with?.

Source
build_extension_index

Build extension index from file_map for fast lookups

Source
clear_all
Source
clear_namespace(ns : Noir::LocatorKeyNamespace)

Drops every runtime-minted key under ns. Runs at a phase boundary with no fibers in flight, so a plain reject is enough — no minted-key bookkeeping to maintain.

Source
content_cache_stats
Source
content_for(path : String) : String | Nil

Returns cached file content or nil if the file was not cached (budget exhausted, caching disabled, or read after cache was cleared). Callers should fall back to File.read on nil.

Source
expanded_file_map

{original, File.expand_path(original)} for every file in file_map, built once and cached. File.expand_path is pure string normalization but non-trivial, and the monorepo helpers in FileHelper re-scan all_files once per base path per analyzer — without this the same path is expanded thousands of times (O(analyzers × bases × files)). Invalidated whenever file_map changes (push / clear).

Source
expanded_path_for(path : String) : String

O(1) path => File.expand_path(path) lookup for files registered in file_map. Analyzers call path_under_root?(file, base) inside base_paths.each { files.each { ... } } loops, so the same file would otherwise be re-expanded once per base (and File.expand_path of a relative path issues a getcwd). Unregistered paths fall back to a live expansion. Shares the lazy lifecycle / invalidation of expanded_file_map.

Source
files_by_basename(basename : String) : Array(String)

Files whose basename is exactly basename (O(1) lookup).

Source
files_by_extension(extension : String) : Array(String)

Get files by extension using the index (O(1) lookup)

Source
get(key : Noir::LocatorKey(String)) : String | Nil

String? rather than the old (String | Array(String)): the union only existed because a bare string key carried no type, so get could not promise which map it was reading. Its one caller worked around it by interpolating the result.

Source
push(key : Noir::LocatorKey(Array(String)), value : String)
Source
register_file(path : String, content : String)

One-shot used by the detector's file reader: push the path into file_map and (budget permitting) cache the content so analyzers can skip the second File.read. Files whose content exceeds the remaining budget are still registered in file_map but not cached, and content_for(path) returns nil for them — callers must keep a File.read fallback.

Source
register_path(path : String)

Register a path with no content — the detector's content-free fast path, for files nothing will read during detection or passive scan.

Invalidates the four derived views but deliberately NOT @file_contents. A newly registered path has no cached content to invalidate, and dropping the cache here would throw away every file read so far. content_for returning nil for a registered path is the contract — register_file skips over-budget files the same way — so callers keep a File.read fallback regardless.

Source
reset_files

Forget every registered file, its cached content, and every derived view.

Source
scan_base_paths
Source
scan_base_paths=(paths : Array(String))

The -b roots of the current scan, published once by NoirRunner.

Convention filters ("is this file under tests/?", "is this bundled output?") must run on the path relative to the scan base, never on the absolute path — otherwise a directory above the base decides the result and the same source tree reports different endpoints depending on where it is checked out. Analyzers get that through Analyzer#base_relative_path; the shared parser layer (Noir::JSRouteExtractor and friends) has no analyzer instance, so it reads the roots from here.

Source
set(key : Noir::LocatorKey(String), value : String)
Source
show_table
Source