module Noir::SkippedFiles

Overview

Every path by which a scan can quietly lose coverage, funnelled into one place.

AnalyzerFailure covers the case where a whole analyzer raised: coverage lost by the tech. The other half of the same problem is a single file raising inside an analyzer that otherwise completed — the per-file rescues in Analyzer#parallel_analyze and Analyzer#scan_files exist precisely so one unreadable or unparsable file costs only itself. That is the right behaviour, but until now the only trace was a --debug line, so the result of a scan that silently dropped a file was byte-identical to one that read everything.

#2612 made that gap wider by adding a ceiling on parse time: a pathologically malformed file now raises and is skipped where it used to take the process down. Loud-and-fatal became quiet-and-partial, which is the better failure mode only if the "partial" part is visible somewhere.

The analysis pass was only ever half the story, though. A scan drops coverage in six other places — an unlistable directory takes its whole subtree, an oversize or unreadable or binary-looking file is filtered out, a symlinked tree is never walked, an export never lands, -P runs with zero rules loaded — and each of those had its own local counter, its own log line, and no route into errors at all. So --strict (documented as "exit 2 if any analyzer failed or skipped a file") reported green on a scan that had lost most of a codebase.

Rather than six more counters, they all record here. #failures is the one list, errors in the output is that list, and degraded in the CLI is "that list is not empty". Adding a seventh drop path is a #record call.

Tallied rather than listed one entry per file: a repository that trips this usually trips it in bulk (a vendored minified bundle, a generated tree), and errors is part of the JSON/YAML/TOML output — one entry per tech with a count and a few example paths says the same thing without letting a broken checkout print thousands of lines.

Extended Modules

Defined in:

models/skipped_files.cr

Constant Summary

DELIVER_SCOPE = "deliver"
DETECT_SCOPE = "detect"

Labels for the drop paths that belong to no analyzer. They land in the same errors[].tech field a real tech name would, because the field answers the same question either way: which part of the scan came up short.

MAX_PATHS_PER_TECH = 5

Example paths kept per tech. Beyond this only the count grows.

PASSIVE_SCAN_SCOPE = "passive-scan"

Instance Method Summary

Instance Method Detail

def clear(phase : Phase) : Nil #

[View source]
def clear : Nil #

Drops everything, both phases. Used by specs and by library callers that want a clean slate; production code clears one phase at a time.


[View source]
def count : Int32 #

Total files (and directories, and symlinks) skipped since the last #clear, across every scope. Excludes #record_gap entries, which count no items.


[View source]
def failures(phase : Phase) : Array(AnalyzerFailure) #

[View source]
def failures : Array(AnalyzerFailure) #

One AnalyzerFailure per tally plus one per recorded gap, so a dropped file, an unwalked directory and an undelivered export all land in the same errors array — and the same --strict exit code — as a dropped analyzer.


[View source]
def record(tech : String, path : String, reason : String, noun : String = "file", phase : Phase = Phase::Analysis) : Nil #

tech may be empty — Analyzer's own tech returns "" and only analyzer_for fills it in, so a base-class caller is still recorded, just unattributed.

noun names what was dropped so the message reads true for callers that skip something other than a file — a directory that would not list, a symlink that is not followed.


[View source]
def record_gap(scope : String, message : String, phase : Phase = Phase::Scan) : Nil #

One-off coverage gap, reported as written. Use #record instead whenever the loss is per-path and can arrive in bulk.


[View source]