From 659b95b86a2d677d946dbd70d2edeb7df8f7ddc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 16:49:16 +0000 Subject: [PATCH 1/2] chore(parser): mark the grammar test module as test code for Hypatia The Hypatia code scan reported `code_safety/panic_macro` (error) and `code_safety/expect_in_hot_path` (warning) in grammar_conformance.rs, on #432. The file is a test-only module (`#[cfg(test)] mod grammar_conformance` in lib.rs), but it is a file of its own below src/, so the scanner reads it as library code. The file now has the 2 `hypatia: allow` directives, with the reason, in the form that other files of the repository use. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u --- src/ephapax-parser/src/grammar_conformance.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ephapax-parser/src/grammar_conformance.rs b/src/ephapax-parser/src/grammar_conformance.rs index 24aaa52..ee2f686 100644 --- a/src/ephapax-parser/src/grammar_conformance.rs +++ b/src/ephapax-parser/src/grammar_conformance.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 // Owner: Jonathan D.A. Jewell // +// hypatia: allow code_safety/panic_macro -- test-only module (`#[cfg(test)] mod grammar_conformance` in lib.rs): a panic with a message is how a test fails. +// hypatia: allow code_safety/expect_in_hot_path -- test-only module, no hot path: `expect` reports a grammar file or a fixture that the test cannot read. +// //! One normative grammar (v2 plan rung E7). //! //! `spec/ephapax-v2-grammar.ebnf` has 2 parts: From e69ad609a5887a307b88cc62e2d05d07228d5359 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:04:41 +0000 Subject: [PATCH 2/2] fix(parser): the grammar tests read the specification file one time Review finding on #432. With `UPDATE_GRAMMAR=1`, one test wrote spec/ephapax-v2-grammar.ebnf again while another test read the same file on a second thread. The reader could get the old text or a part of the new text, so the update command could fail or check stale content. The tests now take the text from one function with a `OnceLock`. The first call reads the file and, in update mode, writes part 1 again; each other test waits for that call and gets the same text. Checked: a file with a damaged part 1 fails in normal mode; in update mode, 5 runs in sequence pass and give back the committed text. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u --- src/ephapax-parser/src/grammar_conformance.rs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/ephapax-parser/src/grammar_conformance.rs b/src/ephapax-parser/src/grammar_conformance.rs index ee2f686..b2cf25f 100644 --- a/src/ephapax-parser/src/grammar_conformance.rs +++ b/src/ephapax-parser/src/grammar_conformance.rs @@ -229,17 +229,31 @@ fn defined(text: &str) -> Vec<&str> { .collect() } +/// The text of the specification file. With `UPDATE_GRAMMAR` set, the +/// first call writes part 1 again. The tests run on more than one +/// thread: each one gets the text from here, so no test reads the file +/// while another one writes it. +fn specification() -> &'static str { + static TEXT: std::sync::OnceLock = std::sync::OnceLock::new(); + TEXT.get_or_init(|| { + let path = ebnf_path(); + let text = std::fs::read_to_string(&path).expect("read the specification grammar"); + if std::env::var_os("UPDATE_GRAMMAR").is_none() { + return text; + } + let (head, _, tail) = split(&text); + let updated = format!("{head}{BEGIN}\n\n{}\n{END}{tail}", generated()); + if updated != text { + std::fs::write(&path, &updated).expect("write the specification grammar"); + } + updated + }) +} + #[test] fn the_specification_grammar_says_what_the_parser_accepts() { - let path = ebnf_path(); - let text = std::fs::read_to_string(&path).expect("read the specification grammar"); - let (head, part, tail) = split(&text); + let (_, part, _) = split(specification()); let want = format!("\n\n{}\n", generated()); - if std::env::var_os("UPDATE_GRAMMAR").is_some() && part != want { - std::fs::write(&path, format!("{head}{BEGIN}{want}{END}{tail}")) - .expect("write the specification grammar"); - return; - } assert!( part == want, "spec/ephapax-v2-grammar.ebnf is not the grammar of ephapax.pest. Run:\n \ @@ -317,8 +331,7 @@ fn each_parser_rule_is_in_a_fixture() { /// fixture of an `[UNIMPLEMENTED]` production. #[test] fn each_design_production_is_an_alias_or_is_refused() { - let text = std::fs::read_to_string(ebnf_path()).expect("read the specification grammar"); - let (head, part, tail) = split(&text); + let (head, part, tail) = split(specification()); assert!(defined(head).is_empty(), "a production before part 1"); let rules = pest_rules();