Skip to content

Commit dbc4c09

Browse files
hyperpolymathclaude
andcommitted
perf(bots): hoist per-call regex compilation into LazyLock statics
Hypatia expect_in_hot_path. Regex::new(<literal>).expect(..) was being called inside function bodies, so the regex recompiled on every invocation of an analyzer that runs per file. Hoisted to module-level LazyLock statics: compiled once, on first use. Touched 6 files across 3 crates: accessibilitybot contrast.rs (6), css.rs (4->3), keyboard.rs (2) finishingbot claims.rs (2), license.rs (1) glambot accessibility.rs (3) glambot additionally needed use regex::Regex; at module scope -- it was function-scoped, which no longer reaches the hoisted statics. The now-redundant function-local imports were removed. Deliberately NOT touched: the 11 files whose expect() already sits inside a LazyLock/OnceLock initialiser (they compile once already -- the rule is wrong about those), and the expect() calls that are panic-on-error rather than per-call compilation (a semaphore acquire, a post-insert lookup, benchmark setup). Those need error handling or a rule fix, not caching. Verified: cargo check exit 0 on all three crates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 21610ed commit dbc4c09

6 files changed

Lines changed: 71 additions & 30 deletions

File tree

‎bots/accessibilitybot/src/analyzers/contrast.rs‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ use crate::analyzers::Analyzer;
1010
use crate::fleet::{Finding, ImpactAssessment, Severity, WcagLevel};
1111
use regex::Regex;
1212
use std::path::Path;
13+
use std::sync::LazyLock;
14+
15+
// Compiled once. These were rebuilt on every call (hypatia expect_in_hot_path).
16+
static COLOR_RE: LazyLock<Regex> =
17+
LazyLock::new(|| Regex::new(r"(?i)(?:^|;|\{)\s*color\s*:\s*([^;}\n]+)").expect("valid regex"));
18+
static BG_RE: LazyLock<Regex> =
19+
LazyLock::new(|| Regex::new(r"(?i)background(?:-color)?\s*:\s*([^;}\n]+)").expect("valid regex"));
20+
static BLOCK_RE: LazyLock<Regex> =
21+
LazyLock::new(|| Regex::new(r"([^{]+)\{([^}]+)\}").expect("valid regex"));
22+
static STYLE_RE: LazyLock<Regex> =
23+
LazyLock::new(|| Regex::new(r#"style\s*=\s*"([^"]+)""#).expect("valid regex"));
24+
static COLOR_RE_4: LazyLock<Regex> =
25+
LazyLock::new(|| Regex::new(r"(?i)(?:^|;)\s*color\s*:\s*([^;]+)").expect("valid regex"));
26+
static BG_RE_5: LazyLock<Regex> =
27+
LazyLock::new(|| Regex::new(r"(?i)background(?:-color)?\s*:\s*([^;]+)").expect("valid regex"));
1328

1429
/// Contrast analyzer for CSS color pairs
1530
pub struct ContrastAnalyzer;
@@ -133,15 +148,11 @@ pub fn contrast_ratio(fg: (u8, u8, u8), bg: (u8, u8, u8)) -> f64 {
133148
/// Analyze CSS content for contrast issues
134149
fn analyze_css(path: &Path, content: &str) -> Vec<Finding> {
135150
let mut findings = Vec::new();
136-
let color_re = Regex::new(
137-
r"(?i)(?:^|;|\{)\s*color\s*:\s*([^;}\n]+)"
138-
).expect("valid regex");
139-
let bg_re = Regex::new(
140-
r"(?i)background(?:-color)?\s*:\s*([^;}\n]+)"
141-
).expect("valid regex");
151+
let color_re = &*COLOR_RE;
152+
let bg_re = &*BG_RE;
142153

143154
// Extract color/background-color pairs within CSS rule blocks
144-
let block_re = Regex::new(r"([^{]+)\{([^}]+)\}").expect("valid regex");
155+
let block_re = &*BLOCK_RE;
145156

146157
for caps in block_re.captures_iter(content) {
147158
let selector = caps[1].trim();
@@ -211,9 +222,9 @@ fn analyze_css(path: &Path, content: &str) -> Vec<Finding> {
211222
/// Analyze inline styles in HTML for contrast issues
212223
fn analyze_inline_styles(path: &Path, content: &str) -> Vec<Finding> {
213224
let mut findings = Vec::new();
214-
let style_re = Regex::new(r#"style\s*=\s*"([^"]+)""#).expect("valid regex");
215-
let color_re = Regex::new(r"(?i)(?:^|;)\s*color\s*:\s*([^;]+)").expect("valid regex");
216-
let bg_re = Regex::new(r"(?i)background(?:-color)?\s*:\s*([^;]+)").expect("valid regex");
225+
let style_re = &*STYLE_RE;
226+
let color_re = &*COLOR_RE_4;
227+
let bg_re = &*BG_RE_5;
217228

218229
for (line_num, line) in content.lines().enumerate() {
219230
if let Some(style_caps) = style_re.captures(line) {

‎bots/accessibilitybot/src/analyzers/css.rs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ use crate::analyzers::Analyzer;
1616
use crate::fleet::{Finding, ImpactAssessment, Severity, WcagLevel};
1717
use regex::Regex;
1818
use std::path::Path;
19+
use std::sync::LazyLock;
20+
21+
// Compiled once. These were rebuilt on every call (hypatia expect_in_hot_path).
22+
static FONT_SIZE_PX_RE: LazyLock<Regex> =
23+
LazyLock::new(|| Regex::new(r"(?i)font-size\s*:\s*(\d+)px").expect("valid regex"));
24+
static LINE_HEIGHT_RE: LazyLock<Regex> =
25+
LazyLock::new(|| Regex::new(r"(?i)line-height\s*:\s*([\d.]+)").expect("valid regex"));
26+
static BLOCK_RE: LazyLock<Regex> =
27+
LazyLock::new(|| Regex::new(r"([^{]+)\{([^}]+)\}").expect("valid regex"));
1928

2029
/// CSS accessibility analyzer
2130
pub struct CssAnalyzer;
@@ -53,7 +62,7 @@ impl Analyzer for CssAnalyzer {
5362

5463
/// Check for font sizes using px instead of rem/em
5564
fn check_px_font_sizes(path: &Path, content: &str, findings: &mut Vec<Finding>) {
56-
let font_size_px_re = Regex::new(r"(?i)font-size\s*:\s*(\d+)px").expect("valid regex");
65+
let font_size_px_re = &*FONT_SIZE_PX_RE;
5766

5867
for (line_num, line) in content.lines().enumerate() {
5968
if let Some(caps) = font_size_px_re.captures(line) {
@@ -83,8 +92,8 @@ fn check_px_font_sizes(path: &Path, content: &str, findings: &mut Vec<Finding>)
8392

8493
/// Check line-height is at least 1.5 for body text
8594
fn check_line_height(path: &Path, content: &str, findings: &mut Vec<Finding>) {
86-
let line_height_re = Regex::new(r"(?i)line-height\s*:\s*([\d.]+)").expect("valid regex");
87-
let block_re = Regex::new(r"([^{]+)\{([^}]+)\}").expect("valid regex");
95+
let line_height_re = &*LINE_HEIGHT_RE;
96+
let block_re = &*BLOCK_RE;
8897

8998
for caps in block_re.captures_iter(content) {
9099
let selector = caps[1].trim();
@@ -203,7 +212,7 @@ fn check_contrast_preference(path: &Path, content: &str, findings: &mut Vec<Find
203212

204213
/// Check for display: none being used where sr-only pattern should be used
205214
fn check_display_none_misuse(path: &Path, content: &str, findings: &mut Vec<Finding>) {
206-
let block_re = Regex::new(r"([^{]+)\{([^}]+)\}").expect("valid regex");
215+
let block_re = &*BLOCK_RE;
207216

208217
for caps in block_re.captures_iter(content) {
209218
let selector = caps[1].trim();

‎bots/accessibilitybot/src/analyzers/keyboard.rs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use crate::fleet::{Finding, ImpactAssessment, Severity, WcagLevel};
1313
use regex::Regex;
1414
use scraper::{Html, Selector};
1515
use std::path::Path;
16+
use std::sync::LazyLock;
17+
18+
// Compiled once. These were rebuilt on every call (hypatia expect_in_hot_path).
19+
static OUTLINE_NONE_RE: LazyLock<Regex> =
20+
LazyLock::new(|| Regex::new(r"(?i)outline\s*:\s*(none|0)\s*[;}\n]").expect("valid regex"));
21+
static STYLE_RE: LazyLock<Regex> =
22+
LazyLock::new(|| Regex::new(r#"style\s*=\s*"[^"]*outline\s*:\s*(none|0)[^"]*""#).expect("valid regex"));
1623

1724
/// Keyboard navigation analyzer
1825
pub struct KeyboardAnalyzer;
@@ -185,7 +192,7 @@ fn check_onclick_no_keyboard(
185192
/// Check for outline: none in CSS (suppresses focus indicator)
186193
fn check_focus_styles_css(path: &Path, content: &str) -> Vec<Finding> {
187194
let mut findings = Vec::new();
188-
let outline_none_re = Regex::new(r"(?i)outline\s*:\s*(none|0)\s*[;}\n]").expect("valid regex");
195+
let outline_none_re = &*OUTLINE_NONE_RE;
189196

190197
for (line_num, line) in content.lines().enumerate() {
191198
if outline_none_re.is_match(line) {
@@ -228,7 +235,7 @@ fn check_inline_focus_suppression(
228235
content: &str,
229236
findings: &mut Vec<Finding>,
230237
) {
231-
let style_re = Regex::new(r#"style\s*=\s*"[^"]*outline\s*:\s*(none|0)[^"]*""#).expect("valid regex");
238+
let style_re = &*STYLE_RE;
232239

233240
for (line_num, line) in content.lines().enumerate() {
234241
if style_re.is_match(line) {

‎bots/finishingbot/src/analyzers/claims.rs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ use regex::Regex;
1010
use std::path::Path;
1111
use tracing::debug;
1212
use walkdir::WalkDir;
13+
use std::sync::LazyLock;
14+
15+
// Compiled once. These were rebuilt on every call (hypatia expect_in_hot_path).
16+
static MEMBER_RE: LazyLock<Regex> =
17+
LazyLock::new(|| Regex::new(r#"members\s*=\s*\[([\s\S]*?)\]"#).expect("Cargo workspace members regex is a valid constant pattern"));
18+
static PATH_RE: LazyLock<Regex> =
19+
LazyLock::new(|| Regex::new(r#""([^"]+)""#).expect("Cargo workspace member-path regex is a valid constant pattern"));
1320

1421
/// Claim verification analyzer
1522
pub struct ClaimsAnalyzer;
@@ -442,12 +449,10 @@ impl ClaimsAnalyzer {
442449
if let Ok(content) = std::fs::read_to_string(cargo_path) {
443450
// Simple check: look for workspace members that don't exist
444451
if content.contains("[workspace]") {
445-
let member_re = Regex::new(r#"members\s*=\s*\[([\s\S]*?)\]"#)
446-
.expect("Cargo workspace members regex is a valid constant pattern");
452+
let member_re = &*MEMBER_RE;
447453
if let Some(caps) = member_re.captures(&content) {
448454
let members_str = &caps[1];
449-
let path_re = Regex::new(r#""([^"]+)""#)
450-
.expect("Cargo workspace member-path regex is a valid constant pattern");
455+
let path_re = &*PATH_RE;
451456
let cargo_dir = cargo_path.parent().unwrap_or_else(|| Path::new("."));
452457
for cap in path_re.captures_iter(members_str) {
453458
let member_path = cargo_dir.join(&cap[1]);

‎bots/finishingbot/src/analyzers/license.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ use regex::Regex;
99
use std::path::Path;
1010
use tracing::debug;
1111
use walkdir::WalkDir;
12+
use std::sync::LazyLock;
13+
14+
// Compiled once. These were rebuilt on every call (hypatia expect_in_hot_path).
15+
static SPDX_PATTERN: LazyLock<Regex> =
16+
LazyLock::new(|| Regex::new(r"(?i)SPDX-License-Identifier:\s*[A-Za-z0-9.-]+").expect("SPDX header regex is a valid constant pattern"));
1217

1318
/// Repositories that deliberately use AGPL-3.0-or-later instead of PMPL.
1419
/// These are co-developed projects with the user's son and use AGPL by deliberate choice.
@@ -222,8 +227,7 @@ impl LicenseAnalyzer {
222227

223228
/// Check for SPDX headers in source files
224229
fn check_spdx_headers(&self, path: &Path, config: &Config, result: &mut AnalysisResult) {
225-
let spdx_pattern = Regex::new(r"(?i)SPDX-License-Identifier:\s*[A-Za-z0-9.-]+")
226-
.expect("SPDX header regex is a valid constant pattern");
230+
let spdx_pattern = &*SPDX_PATTERN;
227231

228232
for entry in WalkDir::new(path)
229233
.follow_links(false)

‎bots/glambot/src/analyzers/accessibility.rs‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ use crate::config::Config;
77
use crate::error::Result;
88
use std::path::Path;
99
use walkdir::WalkDir;
10+
use std::sync::LazyLock;
11+
use regex::Regex;
12+
13+
// Compiled once. These were rebuilt on every call (hypatia expect_in_hot_path).
14+
static MD_IMAGE_RE: LazyLock<Regex> =
15+
LazyLock::new(|| Regex::new(r"!\[(.*?)\]\((.*?)\)").expect("static markdown image regex literal is valid"));
16+
static ADOC_IMAGE_RE: LazyLock<Regex> =
17+
LazyLock::new(|| Regex::new(r"image::(.*?)\[(.*?)\]").expect("static asciidoc image regex literal is valid"));
18+
static MD_LINK_RE: LazyLock<Regex> =
19+
LazyLock::new(|| Regex::new(r"\[(.*?)\]\((.*?)\)").expect("static markdown link regex literal is valid"));
1020

1121
/// Accessibility analyzer
1222
pub struct AccessibilityAnalyzer;
@@ -121,12 +131,9 @@ impl Analyzer for AccessibilityAnalyzer {
121131

122132
impl AccessibilityAnalyzer {
123133
fn check_image_alt_text(&self, path: &Path, config: &Config, result: &mut AnalysisResult) {
124-
use regex::Regex;
125134

126-
let md_image_re = Regex::new(r"!\[(.*?)\]\((.*?)\)")
127-
.expect("static markdown image regex literal is valid");
128-
let adoc_image_re = Regex::new(r"image::(.*?)\[(.*?)\]")
129-
.expect("static asciidoc image regex literal is valid");
135+
let md_image_re = &*MD_IMAGE_RE;
136+
let adoc_image_re = &*ADOC_IMAGE_RE;
130137

131138
for entry in WalkDir::new(path)
132139
.into_iter()
@@ -239,10 +246,8 @@ impl AccessibilityAnalyzer {
239246
}
240247

241248
fn check_link_text(&self, path: &Path, config: &Config, result: &mut AnalysisResult) {
242-
use regex::Regex;
243249

244-
let md_link_re = Regex::new(r"\[(.*?)\]\((.*?)\)")
245-
.expect("static markdown link regex literal is valid");
250+
let md_link_re = &*MD_LINK_RE;
246251

247252
let non_descriptive = ["click here", "here", "read more", "link", "more"];
248253

0 commit comments

Comments
 (0)