module Noir::PathScope

Overview

Path-boundary helpers shared by the analyzers and engines. Monorepo scans (multiple -b base paths) have to answer two questions repeatedly: does a file live under a given root, and which configured base does it belong to? Both must respect path boundaries — a plain String#starts_with? leaks siblings (/app would swallow /app2) — so the comparison runs on File.expand_path-normalized paths.

This logic was copy-pasted across half a dozen call sites (FileHelper, Analyzer, PythonEngine, the Dart helper, the Express router scanner, FastAPI). This module is the single source of truth; a future tweak (Windows separators, symlink handling, …) now lands in one place.

Hot loops should normalize the loop-invariant root once via #normalize_root and compare with #under_normalized_root? rather than paying for File.expand_path of the root per file.

Extended Modules

Defined in:

utils/path_scope.cr

Instance Method Summary

Instance Method Detail

def base_relative(path : String, base_path : String) : String #

The form every convention filter must match on: path's location relative to the scan base that owns it, /-separated and rooted with a leading /.

"Is this file under tests/?", "is this vendored?", "is this build output?" are all statements about a location inside the project. Ask them of the absolute path and the answer depends on where the checkout happens to sit: a clone into ~/work/tests/myapp, or a CI job that checks out under a test/ step directory, matches every file in the project and silently reports nothing. Rooting the result means one includes?("/tests/") also covers a tests/ directory at the top of the project, and keeps every match on a whole path segment (/test/ never matches latest/).

A path outside base_path keeps its own (absolute) form, exactly as get_relative_path returns it — the filters then behave as they did before, which is the conservative choice for a file the scan base does not own.


[View source]
def base_relative(path : String, base_paths : Array(String)) : String #

Multi-base overload: resolves the owning base first. Callers with a per-path base cache (Analyzer#configured_base_for) should use that and call the single-base form instead.


[View source]
def longest_base(path : String, bases : Enumerable(String)) : String | Nil #

The most specific (longest normalized) base in bases that contains path, or nil if none does. Returns the ORIGINAL base string (not its normalized form) so callers can use it as a stable map key.


[View source]
def normalize_root(root : String) : String #

Canonical comparison form of a root: expanded, with any trailing separator stripped (except the filesystem root itself).


[View source]
def relative_under(path : String, base_path : String | Nil) : String #

Remainder of path beneath base_path, or the bare basename when path is outside base_path (or no base given). Backs the Python/Dart test-path conventions, which key off the project-relative path so nested fixture trees don't trip the tests//test/ filters.


[View source]
def under_normalized_root?(expanded_path : String, normalized_root : String) : Bool #

Boundary check against an already-expanded path and an already-normalized root. Use this in per-file loops where the root is loop-invariant (normalize it once with #normalize_root).


[View source]
def under_root?(path : String, root : String) : Bool #

True when path is root or sits beneath it on a path boundary. root is expanded/normalized internally; an empty root matches everything (mirrors the historical starts_with?("")).


[View source]