Files
pbakaus_impeccable/crates/html/tests/rule_pack.rs
T
Paul BakausandClaude Fable 5.1 8ac3886a9c Port: Fix detector URL scans and advisory handling (#709)
Upstream sha fa44839f72.

Advisory handling. `severity` becomes the canonical registry field: the
`advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on
`severity == "advisory"`, and `derive_advisory_flag` stamps the finding's
`advisory: true` from the effective severity, so a per-finding promotion or
demotion carries the flag. The html and browser engines call it after their
severity override; the detect CLI and the hook accept either spelling; the
driver's serializer and the wasm registry exports derive it the same way.
em-dash-overuse moves from `advisory: true` to `severity: "advisory"`.

URL scans. `expand_joined_url_targets` splits an argv value that is entirely
whitespace-separated URLs and leaves paths with spaces alone. The browser
driver reads the readable linked-stylesheet corpus into the HTML pattern
corpora and resolves a finding's selector with `selector_nodes_for_live_dom`
/ `pseudo_element_host_selector`, so an unresolvable selector drops the
finding instead of keeping it page-level. The CSSOM walk itself is page JS:
`browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping
rules flattened, container-query probes, effective keyframes) and puts it in
the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page
route, and the Dom trait carries `linked_stylesheet_text`.

Also `enclosing_css_selector` blanks comments before hunting the previous
declaration delimiter, and `check_typography` reports the uniquely most-used
family instead of every family over a 15% share.

Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns`
is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree
over the shared corpus (432 findings). The two changed lines in
tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were
re-recorded by running origin/main's `checkHtmlPatterns` over the frozen
args; only the comment-polluted selector changed. Goldens re-recorded for
the advisory partition (config-*, fixture gemini/gpt-tells,
numbered-section-labels, scoped-ignore, shape-assembled-illustration,
color, em-dash-entities) and the help text, each cross-checked against the
JS on origin/main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-03 12:24:54 -07:00

143 lines
4.4 KiB
Rust

//! The static HTML engine's half of the rule-pack extension point: the
//! document hook runs on the parsed page after every built-in pass, the
//! engine-wide text hook covers HTML files when no document hook is set, and
//! a run with no pack is identical to the built-in output.
use impeccable_core::findings::{finding_for, Finding};
use impeccable_core::registry::Antipattern;
use impeccable_core::rule_pack::RulePack;
use impeccable_html::dom::StaticDocument;
use impeccable_html::{detect_html_source, DetectHtmlOptions, StaticRulePack};
use std::path::Path;
const MARKER: &str = "TODO(pack)";
static ROWS: &[Antipattern] = &[Antipattern {
id: "testpack/todo-marker",
category: "quality",
scopes: None,
severity: Some("warning"),
name: "Unfinished copy marker",
description: "Text still carries a TODO marker from drafting.",
skill_section: None,
skill_guideline: None,
}];
#[derive(Debug)]
struct TestPack;
static PACK: TestPack = TestPack;
impl RulePack for TestPack {
fn registry(&self) -> &'static [Antipattern] {
ROWS
}
fn check_text(&self, content: &str, file_path: &str, _ext: &str) -> Vec<Finding> {
content
.split('\n')
.enumerate()
.filter(|(_, line)| line.contains(MARKER))
.map(|(i, line)| finding_for(&ROWS[0], file_path, line.trim(), (i + 1) as f64))
.collect()
}
}
impl StaticRulePack for TestPack {
fn check_document(&self, doc: &StaticDocument, file_path: &str) -> Vec<Finding> {
doc.query_selector_all("*")
.into_iter()
.filter(|el| el.direct_text().contains(MARKER))
.map(|el| {
finding_for(
&ROWS[0],
file_path,
&format!("<{}> {}", el.tag_lower(), el.direct_text().trim()),
0.0,
)
})
.collect()
}
}
/// A full page with one built-in finding (the side-tab stripe) plus a line
/// the pack is about.
const PAGE: &str = r#"<!DOCTYPE html>
<html><head><title>t</title><style>
.card { border-left: 4px solid #6366f1; background: #fff; }
</style></head>
<body>
<div class="card">A card</div>
<p>TODO(pack) write the real headline</p>
</body></html>
"#;
fn scan(options: &DetectHtmlOptions<'_>) -> Vec<Finding> {
detect_html_source(PAGE, Path::new("/app/index.html"), options)
}
#[test]
fn document_hook_fires_and_no_pack_is_unchanged() {
let built_in = scan(&DetectHtmlOptions::default());
assert!(
!built_in.is_empty(),
"the fixture must trip a built-in rule"
);
impeccable_core::rule_pack::install(&PACK);
let with_pack = scan(&DetectHtmlOptions {
static_rule_pack: Some(&PACK),
rule_pack: Some(&PACK),
..Default::default()
});
// Built-in findings come first, unchanged, in the same order.
assert_eq!(&with_pack[..built_in.len()], &built_in[..]);
// Exactly one pack finding: the document hook wins over the text hook, so
// a pack that implements both does not report the same file twice.
let extra = &with_pack[built_in.len()..];
assert_eq!(extra.len(), 1);
assert_eq!(extra[0].antipattern, "testpack/todo-marker");
assert_eq!(extra[0].snippet, "<p> TODO(pack) write the real headline");
assert_eq!(extra[0].line, 0.0);
}
#[test]
fn text_hook_covers_html_when_no_document_hook_is_set() {
impeccable_core::rule_pack::install(&PACK);
let with_text_only = scan(&DetectHtmlOptions {
rule_pack: Some(&PACK),
..Default::default()
});
let pack_findings: Vec<&Finding> = with_text_only
.iter()
.filter(|f| f.antipattern.starts_with("testpack/"))
.collect();
assert_eq!(pack_findings.len(), 1);
// The text hook reports the source line, not the element.
assert_eq!(pack_findings[0].line, 7.0);
assert_eq!(
pack_findings[0].snippet,
"<p>TODO(pack) write the real headline</p>"
);
}
#[test]
fn pack_findings_are_waivable_inline() {
impeccable_core::rule_pack::install(&PACK);
let page = PAGE.replace(
"<body>",
"<body>\n<!-- impeccable-disable testpack/todo-marker -->",
);
let findings = detect_html_source(
&page,
Path::new("/app/index.html"),
&DetectHtmlOptions {
static_rule_pack: Some(&PACK),
..Default::default()
},
);
assert!(findings
.iter()
.all(|f| !f.antipattern.starts_with("testpack/")));
}