class CodeLocator
- CodeLocator
- Reference
- Object
Defined in:
models/code_locator.crConstant Summary
-
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 envNOIR_CONTENT_CACHE_DISABLE=trueto disable caching entirely, in which case#content_foralways returns nil and analyzers fall through toFile.read. -
MAX_CONTENT_CACHE_MB =
Int64::MAX // (1024_i64 * 1024) -
Megabyte figures at or above this overflow
Int64once scaled to bytes. Crystal checks integer arithmetic, so the multiply below raised rather than wrapping:NOIR_CONTENT_CACHE_MAX_MB=99999999999999aborted the whole scan with an unhandledOverflowErrorstack trace before the first file was read. Saturate instead — a budget that large already means "cache everything", which is exactly whatInt64::MAXgives.
Constructors
Instance Method Summary
- #all(key : Noir::LocatorKey(Array(String))) : Array(String)
-
#all_files : Array(String)
Every registered path, in registration order.
-
#base_relative(path : String) : String
pathrelative to the scan base that owns it,/-separated and rooted with a leading/. -
#build_basename_index
Build a
basename => pathsindex from file_map. -
#build_extension_index
Build extension index from file_map for fast lookups
- #clear(key : Noir::LocatorKey)
- #clear_all
-
#clear_namespace(ns : Noir::LocatorKeyNamespace)
Drops every runtime-minted key under
ns. - #content_cache_stats : NamedTuple(bytes: Int64, files: Int32, skipped: Int32, budget: Int64)
-
#content_for(path : String) : String | Nil
Returns cached file content or
nilif the file was not cached (budget exhausted, caching disabled, or read after cache was cleared). -
#expanded_file_map : Array(Tuple(String, String))
{original, File.expand_path(original)}for every file infile_map, built once and cached. -
#expanded_path_for(path : String) : String
O(1)
path => File.expand_path(path)lookup for files registered infile_map. -
#files_by_basename(basename : String) : Array(String)
Files whose basename is exactly
basename(O(1) lookup). -
#files_by_extension(extension : String) : Array(String)
Get files by extension using the index (O(1) lookup)
- #get(key : Noir::LocatorKey(String)) : String | Nil
- #push(key : Noir::LocatorKey(Array(String)), value : String)
-
#register_file(path : String, content : String)
One-shot used by the detector's file reader: push the path into
file_mapand (budget permitting) cache the content so analyzers can skip the secondFile.read. -
#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.
-
#reset_files
Forget every registered file, its cached content, and every derived view.
- #scan_base_paths : Array(String)
-
#scan_base_paths=(paths : Array(String))
The
-broots of the current scan, published once byNoirRunner. - #set(key : Noir::LocatorKey(String), value : String)
- #show_table
Constructor Detail
Instance Method Detail
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.
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?.
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.
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.
{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).
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.
Files whose basename is exactly basename (O(1) lookup).
Get files by extension using the index (O(1) lookup)
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.
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.
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.
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.