Forward guard reads an effect name past attributes and doc comments (improvise-94x2); clippy slice fix
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
@@ -258,7 +258,9 @@ expansions and `SetTransientPrefix` in `command/keymap.rs`. All nine are
|
||||
`ViewOnly`, which is the half the `ViewBack` bug was in.
|
||||
|
||||
`improvise-command/src/ui/reduce/vocabulary_is_complete.rs` is that direction.
|
||||
It scans the crate's `src/` for both ways an effect is declared, and requires
|
||||
It scans the crate's `src/` for both ways an effect is declared, reading
|
||||
each name past any attributes and doc comments before it. The macro accepts
|
||||
those, and missing them hid effects (improvise-94x2). It requires
|
||||
each name to be a `Command` or `ModelCommand` variant (read off serde's derive)
|
||||
or to appear in `NOT_ON_THE_WIRE` with a verdict. A verdict is one of four
|
||||
kinds, and the check verifies each kind:
|
||||
|
||||
@@ -4927,3 +4927,18 @@ broken walk therefore fails that test instead of shrinking the set.
|
||||
`an_effect_named_only_in_a_comment_or_string_is_not_an_effect` shares
|
||||
improvise-c7mu's fault. With `code_only` made the identity, it failed with
|
||||
`["SaveAs", "ExportCsv", "Real", "SetYanked", "AlsoReal"]`.
|
||||
|
||||
**improvise-94x2, found by the PR #172 review.** The reader took the
|
||||
identifier *immediately* after `simple_effect!(`. The macro's own matcher is
|
||||
`$(#[$meta:meta])* $name:ident(…)`, so a doc comment before the name (blanked
|
||||
to whitespace by `code_only`), an attribute, or a line break after `(` left
|
||||
nothing to read, and the effect was invisible. The reviewer injected a
|
||||
documented `simple_effect!(\n /// Rival.\n SetBogusRival(bool) …)` into
|
||||
`effect.rs`, and all five tests passed. Re-injected here, it also passed all
|
||||
five before the fix. After the fix
|
||||
`every_effect_is_on_the_wire_or_has_a_verdict` fails naming `SetBogusRival`.
|
||||
The rival was removed afterwards. `name_after` now skips whitespace and every
|
||||
bracket-matched `#[…]` before reading the name.
|
||||
`an_attributed_or_wrapped_effect_is_still_an_effect` pins that on a fixture:
|
||||
a doc comment, two attributes (one with nested brackets), and a wrapped call.
|
||||
With the whitespace skip removed it fails with `[]`.
|
||||
|
||||
@@ -178,10 +178,7 @@ fn effects_named(code: &str) -> Vec<String> {
|
||||
let mut found = Vec::new();
|
||||
for marker in ["impl Effect for ", "simple_effect!("] {
|
||||
for (at, _) in code.match_indices(marker) {
|
||||
let name: String = code[at + marker.len()..]
|
||||
.chars()
|
||||
.take_while(char::is_ascii_alphanumeric)
|
||||
.collect();
|
||||
let name = name_after(&code[at + marker.len()..]);
|
||||
if name.starts_with(|c: char| c.is_ascii_uppercase()) {
|
||||
found.push(name);
|
||||
}
|
||||
@@ -190,6 +187,49 @@ fn effects_named(code: &str) -> Vec<String> {
|
||||
found
|
||||
}
|
||||
|
||||
/// The identifier a marker introduces. `simple_effect!` takes `$(#[$meta])*`
|
||||
/// before the name, and a doc comment is one: skip them (improvise-94x2).
|
||||
fn name_after(rest: &str) -> String {
|
||||
let mut rest = rest.trim_start();
|
||||
while let Some(attr) = rest.strip_prefix("#[") {
|
||||
rest = attr[closing_bracket(attr) + 1..].trim_start();
|
||||
}
|
||||
rest.chars()
|
||||
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where the `]` that closes an attribute's `#[` falls in `inside`, the text
|
||||
/// after it. Nested brackets count; an unclosed one runs to the end.
|
||||
fn closing_bracket(inside: &str) -> usize {
|
||||
let mut depth = 1usize;
|
||||
for (i, c) in inside.char_indices() {
|
||||
match c {
|
||||
'[' => depth += 1,
|
||||
']' if depth == 1 => return i,
|
||||
']' => depth -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
inside.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
/// The spelling the macro itself documents, a doc comment and an attribute
|
||||
/// before the name, and a line break after `(`. Each hid an effect.
|
||||
#[test]
|
||||
fn an_attributed_or_wrapped_effect_is_still_an_effect() {
|
||||
let text = concat!(
|
||||
"simple_effect!(\n",
|
||||
" /// Documented, as the macro's own matcher allows.\n",
|
||||
" #[allow(dead_code)]\n",
|
||||
" #[cfg_attr(all(), doc = \"x\")]\n",
|
||||
" Documented(bool) => |app| x, K\n",
|
||||
");\n",
|
||||
"simple_effect!(\n Wrapped(bool) => |app| y, K);\n",
|
||||
);
|
||||
assert_eq!(effects_in(text), vec!["Documented", "Wrapped"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_effect_named_only_in_a_comment_or_string_is_not_an_effect() {
|
||||
let text = concat!(
|
||||
|
||||
@@ -264,7 +264,7 @@ mod tests {
|
||||
std::fs::write(src.join("p.rs"), "fn production() {}\n").unwrap();
|
||||
std::fs::write(src.join("t.rs"), "fn test_only() {}\nmod deeper;\n").unwrap();
|
||||
std::fs::write(src.join("t").join("deeper.rs"), "fn test_too() {}\n").unwrap();
|
||||
let mut found: Vec<String> = production_code(&[src.clone()])
|
||||
let mut found: Vec<String> = production_code(std::slice::from_ref(&src))
|
||||
.into_iter()
|
||||
.map(|(file, _)| file.strip_prefix(&src).unwrap().display().to_string())
|
||||
.collect();
|
||||
|
||||
Reference in New Issue
Block a user