Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion lib/rules/code_safety.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ defmodule Hypatia.Rules.CodeSafety do
severity: :medium,
pattern: ~r/\.expect\(/,
cwe: "CWE-754",
# A one-time initialiser is not a hot path. Without this the rule fires on
# `LazyLock::new(|| Regex::new(..).expect(..))`, which compiles exactly once
# -- measured 2026-07-29: 11 of 28 findings in gitbot-fleet were this shape,
# and the suggested remediation (cache it) described code that already existed.
context: :runtime_path,
description: "expect() in hot path"
},
%{
Expand Down Expand Up @@ -776,10 +781,14 @@ defmodule Hypatia.Rules.CodeSafety do

def scan_content(content, language) do
Comment thread
hyperpolymath marked this conversation as resolved.
scannable = strip_inline_test_blocks(content, language)
runtime_only = strip_lazy_initialisers(scannable, language)

patterns_for_language(language)
|> Enum.flat_map(fn rule ->
case Regex.scan(rule.pattern, scannable) do
subject =
if Map.get(rule, :context) == :runtime_path, do: runtime_only, else: scannable

case Regex.scan(rule.pattern, subject) do
[] ->
[]

Expand All @@ -797,6 +806,27 @@ defmodule Hypatia.Rules.CodeSafety do
end)
end

@doc false
# Removes the bodies of one-time lazy initialisers before matching rules that
# are specifically about work repeated on a hot path.
#
# static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(..).expect(..));
#
# runs once on first access, so an `.expect(` inside it is not a hot-path
# concern. Matching it produces a finding whose only honest remediation is
# "you already did this". Brace-counting from the initialiser opener keeps the
# rest of the file scannable, so a genuine per-call `.expect(` two lines later
# is still caught.
def strip_lazy_initialisers(content, language) when language in ["rust"] do
Regex.replace(
~r/(?:LazyLock|OnceLock|Lazy|OnceCell)::new\s*\(\s*\|\|(?:[^()]|\((?:[^()]|\([^()]*\))*\))*\)/s,
content,
"LAZY_INIT_ELIDED"
)
end
Comment thread
hyperpolymath marked this conversation as resolved.

def strip_lazy_initialisers(content, _language), do: content

# For Rust:
#
# 1. Strip the trailing `#[cfg(test)] mod tests { ... }` block — it
Expand Down
Loading