mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 01:41:18 +00:00
test(ui): the Page presses guard also checks who calls a hit test
CodeRabbit: a press handler could call `link_hit(press_point)` and act on the answer without touching the router, and the guard would pass. Now a hit test (`hit` or `…_hit`, called by path or bare) may be called only from the functions in HIT_TESTERS: `PageHits::at`, the one place that asks at the press point, and three that ask where the pointer is (a link or form button released over what it was pressed on, a field's tooltip, comment hover). A method after its `.` is left out: `PageCx::hit` finds a comment, not a `Hit`. Seen failing with a path call and a bare call at the press point put into `forms_ui.rs`, and with `comments::grabs`' call removed (stale entry). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 file changed
+59
-14
@@ -263,9 +263,9 @@ fn uses(line: &str, name: &str) -> bool {
|
||||
line.match_indices(name).any(|(at, _)| name.starts_with('.') || !line[..at].ends_with(|c: char| c.is_alphanumeric() || c == '_'))
|
||||
}
|
||||
|
||||
/// Each use of one of `names` in the page modules' code (their tests left out), as (file, the
|
||||
/// top-level function or `impl` method it is in, line, name).
|
||||
fn page_module_uses<'a>(names: &[&'a str]) -> Vec<(&'static str, Option<String>, usize, &'a str)> {
|
||||
/// Each name `find` finds in a line of the page modules' code (their tests left out), as (file,
|
||||
/// the top-level function or `impl` method it is in, line, name).
|
||||
fn page_module_matches(find: impl Fn(&str) -> Vec<String>) -> Vec<(&'static str, Option<String>, usize, String)> {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut found = Vec::new();
|
||||
for file in PAGE_MODULES {
|
||||
@@ -285,21 +285,31 @@ fn page_module_uses<'a>(names: &[&'a str]) -> Vec<(&'static str, Option<String>,
|
||||
if line.trim_start().starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
for name in names.iter().filter(|name| uses(line, name)) {
|
||||
found.push((file, function.map(str::to_owned), n + 1, *name));
|
||||
for name in find(line) {
|
||||
found.push((file, function.map(str::to_owned), n + 1, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Each use of one of `names` in the page modules' code (their tests left out).
|
||||
fn page_module_uses(names: &[&str]) -> Vec<(&'static str, Option<String>, usize, String)> {
|
||||
page_module_matches(|line| names.iter().filter(|name| uses(line, name)).map(|name| (*name).to_owned()).collect())
|
||||
}
|
||||
|
||||
/// Each use of one of `names` in the page modules' code (their tests left out) outside the
|
||||
/// functions `allowed` to, as "src/file:line: `name` why"; then each allowed function that uses
|
||||
/// none of them any more, as "src/file: fn function unused".
|
||||
fn misuses(names: &[&str], allowed: &[(&str, &str, &str)], why: &str, unused: &str) -> Vec<String> {
|
||||
misuses_in(page_module_uses(names), allowed, why, unused)
|
||||
}
|
||||
|
||||
/// [`misuses`], of what was `found`.
|
||||
fn misuses_in(found: Vec<(&str, Option<String>, usize, String)>, allowed: &[(&str, &str, &str)], why: &str, unused: &str) -> Vec<String> {
|
||||
let mut problems = Vec::new();
|
||||
let mut needed = vec![false; allowed.len()];
|
||||
for (file, function, n, name) in page_module_uses(names) {
|
||||
for (file, function, n, name) in found {
|
||||
match allowed.iter().position(|(f, function_name, _)| *f == file && function.as_deref() == Some(*function_name)) {
|
||||
Some(k) => needed[k] = true,
|
||||
None => problems.push(format!("src/{file}:{n}: `{name}` {why}")),
|
||||
@@ -385,6 +395,33 @@ const ROUTER_WRITERS: [(&str, &str, &str); 3] = [
|
||||
/// Ways to make a `Hit`.
|
||||
const HIT_BUILDS: [&str; 2] = ["Hit::new(", "Hit {"];
|
||||
|
||||
/// Whether `name` is a hit test's: `hit`, or `…_hit`.
|
||||
fn is_hit_test(name: &str) -> bool {
|
||||
name == "hit" || name.ends_with("_hit")
|
||||
}
|
||||
|
||||
/// The hit tests `line` calls, by path or bare. A method, after its `.`, is left out:
|
||||
/// `PageCx::hit` finds the comment under a point, not a `Hit`.
|
||||
fn hit_test_calls(line: &str) -> Vec<String> {
|
||||
line.match_indices("hit(")
|
||||
.filter_map(|(at, _)| {
|
||||
let end = at + "hit".len();
|
||||
let start = line[..end].char_indices().rev().find(|(_, c)| !(c.is_alphanumeric() || *c == '_')).map_or(0, |(i, c)| i + c.len_utf8());
|
||||
let (before, name) = (&line[..start], &line[start..end]);
|
||||
(is_hit_test(name) && !before.ends_with('.') && !before.ends_with("fn ")).then(|| name.to_owned())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The functions that call a hit test: (file, function, why). Only `PageHits::at` asks at the
|
||||
/// press point; the others ask where the pointer is.
|
||||
const HIT_TESTERS: [(&str, &str, &str); 4] = [
|
||||
("canvas.rs", "at", "`PageHits::at`: every module's hits at a point, for the pointer's cursor and tip and, once, the press's owner"),
|
||||
("canvas.rs", "page_overlays", "a link's click counts only released over the link its press was on"),
|
||||
("tools/forms_ui.rs", "page_input", "the field's tooltip under the pointer, and a click counts only released over the widget its press was on"),
|
||||
("tools/comments/input.rs", "grabs", "with no button down, whether a comment would take a press under the pointer, for hover painting"),
|
||||
];
|
||||
|
||||
/// What ignores the press the pages hold until the button is up.
|
||||
const PRESS_IGNORES: [&str; 1] = ["press_cancelled = true"];
|
||||
|
||||
@@ -396,7 +433,8 @@ const PRESS_IGNORERS: [(&str, &str, &str); 1] = [("canvas.rs", "cancel_on_escape
|
||||
/// so what the cursor promised is what the press does. Where each module tested the press
|
||||
/// point again, or asked another whether it wanted the press, the answers disagreed: a press on
|
||||
/// one check box released over the next ticked the second, and a drag could be taken over
|
||||
/// halfway. Escape is the one way to drop a press and ignore it until the button is up.
|
||||
/// halfway. Only `PageHits::at` asks the hit tests at the press point. Escape is the one way to
|
||||
/// drop a press and ignore it until the button is up.
|
||||
#[test]
|
||||
fn page_modules_act_on_a_press_only_through_the_router() {
|
||||
let mut problems = misuses(
|
||||
@@ -417,15 +455,22 @@ fn page_modules_act_on_a_press_only_through_the_router() {
|
||||
));
|
||||
// A hit test is a function named `hit` or `…_hit`: one answer to "what of mine is here?",
|
||||
// which gives the module's cursor, its tip and its press.
|
||||
problems.extend(misuses_in(
|
||||
page_module_matches(hit_test_calls),
|
||||
&HIT_TESTERS,
|
||||
"calls a hit test. The hits at the press point decide its owner once, in `PageHits::at`: act on the press when \
|
||||
`view.router.owner(page)` says it is yours, rather than testing its point again. Testing where the pointer \
|
||||
is (a tooltip, a click released over what it was pressed on)? Add the function to HIT_TESTERS in \
|
||||
src/canvas/page_pointer/tests.rs, with why.",
|
||||
"calls no hit test any more: remove it from HIT_TESTERS.",
|
||||
));
|
||||
let builds = page_module_uses(&HIT_BUILDS);
|
||||
assert!(!builds.is_empty(), "no page module makes a `Hit` any more: update HIT_BUILDS");
|
||||
problems.extend(builds.into_iter().filter(|(_, function, _, _)| !function.as_deref().is_some_and(|f| f == "hit" || f.ends_with("_hit"))).map(
|
||||
|(file, _, n, name)| {
|
||||
format!(
|
||||
"src/{file}:{n}: `{name}` makes a hit outside a hit test. A page module answers \"what of mine is \
|
||||
problems.extend(builds.into_iter().filter(|(_, function, _, _)| !function.as_deref().is_some_and(is_hit_test)).map(|(file, _, n, name)| {
|
||||
format!(
|
||||
"src/{file}:{n}: `{name}` makes a hit outside a hit test. A page module answers \"what of mine is \
|
||||
here?\" in one function, named `hit` or `…_hit`, so its cursor, its tip and its press agree."
|
||||
)
|
||||
},
|
||||
));
|
||||
)
|
||||
}));
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
Reference in new issue
Block a user