Skip to main content

max / pter

Adopt universal clippy/lint baseline; drive warnings to zero Add the pedantic + unreachable_pub lint baseline (the everycycle baseline, with an allow-list tuned from a measured breakdown across server/multithreaded/pter). First repo of the cross-repo rollout; this commit is the reference pattern. - Cargo.toml: [lints.rust] unused/unreachable_pub = warn; [lints.clippy] pedantic = warn with the shared allow-list block. - Cleared all 60 resulting warnings: clippy --fix handled the machine- applicable ones (unreachable_pub demotions, needless_raw_string_hashes, uninlined_format_args, semicolons, closures); by hand: Copy on the BlockKind/InlineKind tag enums, one merged match arm, one write! in a bench. clippy --all-targets clean, cargo fmt clean, tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 02:49 UTC
Commit: b04e09341fb4286778f613353a9c0d29a075ea01
Parent: c00e402
8 files changed, +81 insertions, -47 deletions
M Cargo.toml +32
@@ -18,3 +18,35 @@ criterion = { version = "0.8", features = ["html_reports"] }
18 18 [[bench]]
19 19 name = "convert_bench"
20 20 harness = false
21 +
22 + [lints.rust]
23 + unused = "warn"
24 + unreachable_pub = "warn"
25 +
26 + [lints.clippy]
27 + pedantic = { level = "warn", priority = -1 }
28 + # Allow-list tuned from a measured breakdown across server/multithreaded/pter
29 + # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
30 + # else in `pedantic` stays a warning. Keep this block identical across repos.
31 + module_name_repetitions = "allow"
32 + # Doc lints — no docs-completeness push underway.
33 + missing_errors_doc = "allow"
34 + missing_panics_doc = "allow"
35 + doc_markdown = "allow"
36 + # Numeric casts — endemic and mostly intentional in size/byte math.
37 + cast_possible_truncation = "allow"
38 + cast_sign_loss = "allow"
39 + cast_precision_loss = "allow"
40 + cast_possible_wrap = "allow"
41 + cast_lossless = "allow"
42 + # Subjective structure/style nags — high churn, low signal.
43 + must_use_candidate = "allow"
44 + too_many_lines = "allow"
45 + struct_excessive_bools = "allow"
46 + similar_names = "allow"
47 + items_after_statements = "allow"
48 + single_match_else = "allow"
49 + # Frequent false-positives in TUI/router-heavy code across the ecosystem.
50 + match_same_arms = "allow"
51 + unnecessary_wraps = "allow"
52 + type_complexity = "allow"
@@ -1,4 +1,5 @@
1 1 use criterion::{Criterion, criterion_group, criterion_main};
2 + use std::fmt::Write as _;
2 3 use std::hint::black_box;
3 4
4 5 fn simple_email() -> &'static str {
@@ -59,7 +60,7 @@ fn large_email() -> String {
59 60 Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>";
60 61 let mut html = String::from("<html><body>");
61 62 for i in 0..100 {
62 - html.push_str(&format!("<h3>Section {}</h3>", i));
63 + let _ = write!(html, "<h3>Section {i}</h3>");
63 64 html.push_str(paragraph);
64 65 }
65 66 html.push_str("</body></html>");
@@ -69,28 +70,28 @@ fn large_email() -> String {
69 70 fn bench_simple(c: &mut Criterion) {
70 71 let html = simple_email();
71 72 c.bench_function("simple_email", |b| {
72 - b.iter(|| pter::convert(black_box(html)))
73 + b.iter(|| pter::convert(black_box(html)));
73 74 });
74 75 }
75 76
76 77 fn bench_newsletter(c: &mut Criterion) {
77 78 let html = newsletter_email();
78 79 c.bench_function("newsletter_layout_tables", |b| {
79 - b.iter(|| pter::convert(black_box(html)))
80 + b.iter(|| pter::convert(black_box(html)));
80 81 });
81 82 }
82 83
83 84 fn bench_reply_chain(c: &mut Criterion) {
84 85 let html = reply_chain();
85 86 c.bench_function("reply_chain_nested", |b| {
86 - b.iter(|| pter::convert(black_box(html)))
87 + b.iter(|| pter::convert(black_box(html)));
87 88 });
88 89 }
89 90
90 91 fn bench_large(c: &mut Criterion) {
91 92 let html = large_email();
92 93 c.bench_function("large_100_sections", |b| {
93 - b.iter(|| pter::convert(black_box(&html)))
94 + b.iter(|| pter::convert(black_box(&html)));
94 95 });
95 96 }
96 97
M src/convert.rs +3 -4
@@ -246,12 +246,11 @@ fn handle_block(el: ElementRef, ctx: &mut Context, kind: BlockKind) {
246 246
247 247 // Determine bullet or number
248 248 let marker = match ctx.list_stack.last_mut() {
249 - Some(ListType::Unordered) => "- ".to_string(),
250 249 Some(ListType::Ordered(n)) => {
251 250 *n += 1;
252 251 format!("{}. ", *n)
253 252 }
254 - None => "- ".to_string(),
253 + Some(ListType::Unordered) | None => "- ".to_string(),
255 254 };
256 255 ctx.push(&marker);
257 256 walk_children(el, ctx);
@@ -742,7 +741,7 @@ mod tests {
742 741 </td></tr></table>";
743 742 let md = convert(html);
744 743 assert!(md.contains("Inner content"));
745 - assert!(!md.contains("|"));
744 + assert!(!md.contains('|'));
746 745 }
747 746
748 747 #[test]
@@ -750,7 +749,7 @@ mod tests {
750 749 let html = r#"<table role="presentation"><tr><td>Content</td><td>&nbsp;</td></tr></table>"#;
751 750 let md = convert(html);
752 751 assert!(md.contains("Content"));
753 - assert!(!md.contains("|"));
752 + assert!(!md.contains('|'));
754 753 }
755 754
756 755 #[test]
M src/elements.rs +8 -6
@@ -1,7 +1,7 @@
1 1 use scraper::node::Element;
2 2
3 3 /// What kind of markdown wrapper an element produces.
4 - pub enum ElementAction {
4 + pub(crate) enum ElementAction {
5 5 /// Skip this element and all its children entirely.
6 6 Skip,
7 7 /// Render children only, no wrapper (transparent element).
@@ -12,7 +12,8 @@ pub enum ElementAction {
12 12 Inline(InlineKind),
13 13 }
14 14
15 - pub enum BlockKind {
15 + #[derive(Clone, Copy)]
16 + pub(crate) enum BlockKind {
16 17 Paragraph,
17 18 Heading(u8),
18 19 Blockquote,
@@ -25,7 +26,8 @@ pub enum BlockKind {
25 26 Div,
26 27 }
27 28
28 - pub enum InlineKind {
29 + #[derive(Clone, Copy)]
30 + pub(crate) enum InlineKind {
29 31 Bold,
30 32 Italic,
31 33 Strikethrough,
@@ -38,7 +40,7 @@ pub enum InlineKind {
38 40 }
39 41
40 42 /// Classify an HTML element into the action pter should take.
41 - pub fn classify(el: &Element) -> ElementAction {
43 + pub(crate) fn classify(el: &Element) -> ElementAction {
42 44 match el.name() {
43 45 // Skip entirely
44 46 "script" | "style" | "head" | "meta" | "link" | "title" | "noscript" => ElementAction::Skip,
@@ -83,7 +85,7 @@ pub fn classify(el: &Element) -> ElementAction {
83 85
84 86 /// Check if an <img> element is a tracking pixel.
85 87 /// Returns true if it should be skipped.
86 - pub fn is_tracking_pixel(el: &Element) -> bool {
88 + pub(crate) fn is_tracking_pixel(el: &Element) -> bool {
87 89 let width = el.attr("width");
88 90 let height = el.attr("height");
89 91
@@ -128,7 +130,7 @@ pub fn is_tracking_pixel(el: &Element) -> bool {
128 130 ///
129 131 /// Catches display:none, visibility:hidden, and spacer tricks
130 132 /// like font-size:0 or line-height:0 (commonly used in email templates).
131 - pub fn is_hidden(el: &Element) -> bool {
133 + pub(crate) fn is_hidden(el: &Element) -> bool {
132 134 if let Some(style) = el.attr("style") {
133 135 let s = style.to_lowercase();
134 136 if s.contains("display:none")
M src/replies.rs +11 -11
@@ -10,7 +10,7 @@ use scraper::node::Node;
10 10 /// An element is a reply boundary if it's a container that wraps quoted
11 11 /// content from a previous message in the thread. The converter treats
12 12 /// these identically to `<blockquote>` — children get `>` prefixed.
13 - pub fn is_reply_boundary(el: ElementRef) -> bool {
13 + pub(crate) fn is_reply_boundary(el: ElementRef) -> bool {
14 14 let element = el.value();
15 15 let name = element.name();
16 16
@@ -44,7 +44,7 @@ pub fn is_reply_boundary(el: ElementRef) -> bool {
44 44 ///
45 45 /// Returns the attribution text (e.g. "On Mon, Jan 5, Alice wrote:") if found,
46 46 /// so the converter can render it above the quoted block.
47 - pub fn find_attribution(el: ElementRef) -> Option<String> {
47 + pub(crate) fn find_attribution(el: ElementRef) -> Option<String> {
48 48 // Check the element's own leading text for attribution patterns
49 49 for child in el.children() {
50 50 match child.value() {
@@ -190,7 +190,7 @@ fn previous_sibling_text(el: ElementRef) -> Option<String> {
190 190 ///
191 191 /// This catches `<hr>` or styled divs that act as visual separators
192 192 /// before reply content (common in Outlook "From: ... Sent: ..." blocks).
193 - pub fn is_outlook_separator(el: ElementRef) -> bool {
193 + pub(crate) fn is_outlook_separator(el: ElementRef) -> bool {
194 194 let element = el.value();
195 195
196 196 // Outlook uses a specific pattern: a div containing
@@ -431,7 +431,7 @@ mod tests {
431 431
432 432 #[test]
433 433 fn find_attribution_in_leading_text() {
434 - let html = r#"<div>On Mon, Alice wrote:<blockquote>quoted</blockquote></div>"#;
434 + let html = r"<div>On Mon, Alice wrote:<blockquote>quoted</blockquote></div>";
435 435 let (doc, sel) = parse_and_select(html, "div");
436 436 let el = doc.select(&sel).next().unwrap();
437 437 let attr = find_attribution(el);
@@ -441,7 +441,7 @@ mod tests {
441 441
442 442 #[test]
443 443 fn find_attribution_none_when_no_match() {
444 - let html = r#"<div>Just regular text here, nothing fancy.</div>"#;
444 + let html = r"<div>Just regular text here, nothing fancy.</div>";
445 445 let (doc, sel) = parse_and_select(html, "div");
446 446 let el = doc.select(&sel).next().unwrap();
447 447 assert!(find_attribution(el).is_none());
@@ -452,7 +452,7 @@ mod tests {
452 452 // Element-then-text: the Text(_) arm should still match leading text BEFORE
453 453 // hitting any element. With a leading element, the loop should `break`
454 454 // out without inspecting later text. Catches "delete match arm Node::Element(_)".
455 - let html = r#"<div><span>hi</span>On Mon, Alice wrote:</div>"#;
455 + let html = r"<div><span>hi</span>On Mon, Alice wrote:</div>";
456 456 let (doc, sel) = parse_and_select(html, "div");
457 457 let el = doc.select(&sel).next().unwrap();
458 458 // Leading content is an element, not text — and the later text falls outside
@@ -475,7 +475,7 @@ mod tests {
475 475
476 476 #[test]
477 477 fn boundary_div_with_attribution_then_blockquote() {
478 - let html = r#"<div>On Mon, Alice wrote:<blockquote>quoted</blockquote></div>"#;
478 + let html = r"<div>On Mon, Alice wrote:<blockquote>quoted</blockquote></div>";
479 479 let (doc, sel) = parse_and_select(html, "div");
480 480 let el = doc.select(&sel).next().unwrap();
481 481 assert!(is_reply_boundary(el));
@@ -486,7 +486,7 @@ mod tests {
486 486 // A bare blockquote-wrapping div without attribution text is not a boundary.
487 487 // Catches "replace has_attribution_then_quote -> bool with false" (would
488 488 // make this still pass, but the positive case above would fail).
489 - let html = r#"<div><blockquote>quoted</blockquote></div>"#;
489 + let html = r"<div><blockquote>quoted</blockquote></div>";
490 490 let (doc, sel) = parse_and_select(html, "div");
491 491 let el = doc.select(&sel).next().unwrap();
492 492 assert!(!is_reply_boundary(el));
@@ -496,7 +496,7 @@ mod tests {
496 496 fn boundary_div_attribution_no_blockquote_is_false() {
497 497 // Attribution text but no blockquote → not a boundary.
498 498 // Catches the L151 == mutation (would treat any element as blockquote).
499 - let html = r#"<div>On Mon, Alice wrote:<p>not a quote</p></div>"#;
499 + let html = r"<div>On Mon, Alice wrote:<p>not a quote</p></div>";
500 500 let (doc, sel) = parse_and_select(html, "div");
501 501 let el = doc.select(&sel).next().unwrap();
502 502 assert!(!is_reply_boundary(el));
@@ -506,7 +506,7 @@ mod tests {
506 506 fn boundary_div_attribution_br_blockquote() {
507 507 // Attribution → <br> → blockquote. The <br> must be skipped.
508 508 // Catches the L155 != mutation in br-handling.
509 - let html = r#"<div>On Mon, Alice wrote:<br><blockquote>quoted</blockquote></div>"#;
509 + let html = r"<div>On Mon, Alice wrote:<br><blockquote>quoted</blockquote></div>";
510 510 let (doc, sel) = parse_and_select(html, "div");
511 511 let el = doc.select(&sel).next().unwrap();
512 512 assert!(is_reply_boundary(el));
@@ -516,7 +516,7 @@ mod tests {
516 516 fn boundary_div_non_br_element_before_attribution_is_false() {
517 517 // Non-br element BEFORE finding attribution → early return false.
518 518 // Catches the L157 `!` deletion.
519 - let html = r#"<div><p>preface</p>On Mon, Alice wrote:<blockquote>q</blockquote></div>"#;
519 + let html = r"<div><p>preface</p>On Mon, Alice wrote:<blockquote>q</blockquote></div>";
520 520 let (doc, sel) = parse_and_select(html, "div");
521 521 let el = doc.select(&sel).next().unwrap();
522 522 assert!(!is_reply_boundary(el));
M src/tables.rs +3 -3
@@ -10,7 +10,7 @@ use scraper::ElementRef;
10 10 /// - Has multiple rows where multiple cells contain substantive text
11 11 ///
12 12 /// Everything else is treated as a layout table and unwrapped.
13 - pub fn is_data_table(table: ElementRef) -> bool {
13 + pub(crate) fn is_data_table(table: ElementRef) -> bool {
14 14 let el = table.value();
15 15
16 16 // role attribute
@@ -70,7 +70,7 @@ fn has_substantive_text(el: ElementRef) -> bool {
70 70 ///
71 71 /// Returns (headers, rows) where each is a Vec of cell text strings.
72 72 /// If no `<thead>`/`<th>` row exists, the first row is used as headers.
73 - pub fn extract_table_data(table: ElementRef) -> (Vec<String>, Vec<Vec<String>>) {
73 + pub(crate) fn extract_table_data(table: ElementRef) -> (Vec<String>, Vec<Vec<String>>) {
74 74 let mut headers: Vec<String> = Vec::new();
75 75 let mut rows: Vec<Vec<String>> = Vec::new();
76 76
@@ -140,7 +140,7 @@ fn has_th_cells(tr: ElementRef) -> bool {
140 140 }
141 141
142 142 /// Render a data table as a GFM markdown table.
143 - pub fn render_markdown_table(headers: &[String], rows: &[Vec<String>]) -> String {
143 + pub(crate) fn render_markdown_table(headers: &[String], rows: &[Vec<String>]) -> String {
144 144 if headers.is_empty() {
145 145 return String::new();
146 146 }
@@ -3,7 +3,7 @@
3 3 /// - Collapse runs of 3+ newlines into 2 (one blank line)
4 4 /// - Trim leading/trailing whitespace
5 5 /// - Remove trailing whitespace from each line
6 - pub fn normalize(input: &str) -> String {
6 + pub(crate) fn normalize(input: &str) -> String {
7 7 let mut result = String::with_capacity(input.len());
8 8 let mut consecutive_newlines = 0u32;
9 9
@@ -20,7 +20,7 @@ pub fn normalize(input: &str) -> String {
20 20 }
21 21
22 22 // Trim trailing whitespace from each line
23 - let lines: Vec<&str> = result.lines().map(|l| l.trim_end()).collect();
23 + let lines: Vec<&str> = result.lines().map(str::trim_end).collect();
24 24 let joined = lines.join("\n");
25 25 joined.trim().to_string()
26 26 }
@@ -2,7 +2,7 @@ use pter::convert;
2 2
3 3 #[test]
4 4 fn simple_email() {
5 - let html = r#"
5 + let html = r"
6 6 <html>
7 7 <head><title>Email</title></head>
8 8 <body>
@@ -12,7 +12,7 @@ fn simple_email() {
12 12 <p>Best,<br>Alice</p>
13 13 </body>
14 14 </html>
15 - "#;
15 + ";
16 16
17 17 let md = convert(html);
18 18 assert!(md.contains("# Meeting Tomorrow"));
@@ -56,14 +56,14 @@ fn email_with_tracking_pixels() {
56 56
57 57 #[test]
58 58 fn email_with_quoted_reply() {
59 - let html = r#"
59 + let html = r"
60 60 <body>
61 61 <p>Thanks, that works for me.</p>
62 62 <blockquote>
63 63 <p>Can we meet at 3pm instead?</p>
64 64 </blockquote>
65 65 </body>
66 - "#;
66 + ";
67 67
68 68 let md = convert(html);
69 69 assert!(md.contains("Thanks, that works for me."));
@@ -72,14 +72,14 @@ fn email_with_quoted_reply() {
72 72
73 73 #[test]
74 74 fn email_with_signature_line() {
75 - let html = r#"
75 + let html = r"
76 76 <body>
77 77 <p>See you then.</p>
78 78 <hr>
79 79 <p>Alice Smith</p>
80 80 <p>Engineering Lead</p>
81 81 </body>
82 - "#;
82 + ";
83 83
84 84 let md = convert(html);
85 85 assert!(md.contains("See you then."));
@@ -89,7 +89,7 @@ fn email_with_signature_line() {
89 89
90 90 #[test]
91 91 fn deeply_nested_blockquotes() {
92 - let html = r#"
92 + let html = r"
93 93 <body>
94 94 <p>Got it.</p>
95 95 <blockquote>
@@ -102,7 +102,7 @@ fn deeply_nested_blockquotes() {
102 102 </blockquote>
103 103 </blockquote>
104 104 </body>
105 - "#;
105 + ";
106 106
107 107 let md = convert(html);
108 108 assert!(md.contains("Got it."));
@@ -113,7 +113,7 @@ fn deeply_nested_blockquotes() {
113 113
114 114 #[test]
115 115 fn complex_list_structure() {
116 - let html = r#"
116 + let html = r"
117 117 <body>
118 118 <p>Action items:</p>
119 119 <ol>
@@ -126,7 +126,7 @@ fn complex_list_structure() {
126 126 <li>Deploy to staging</li>
127 127 </ol>
128 128 </body>
129 - "#;
129 + ";
130 130
131 131 let md = convert(html);
132 132 assert!(md.contains("Action items:"));
@@ -174,7 +174,7 @@ fn hidden_content_stripped() {
174 174
175 175 #[test]
176 176 fn script_and_style_fully_removed() {
177 - let html = r#"
177 + let html = r"
178 178 <html>
179 179 <head>
180 180 <style>body { color: red; }</style>
@@ -185,7 +185,7 @@ fn script_and_style_fully_removed() {
185 185 <script>document.write('injected')</script>
186 186 </body>
187 187 </html>
188 - "#;
188 + ";
189 189
190 190 let md = convert(html);
191 191 assert_eq!(md, "Safe content");
@@ -234,7 +234,7 @@ fn newsletter_table_layout() {
234 234
235 235 #[test]
236 236 fn data_table_preserved() {
237 - let html = r#"
237 + let html = r"
238 238 <body>
239 239 <p>Order summary:</p>
240 240 <table>
@@ -245,7 +245,7 @@ fn data_table_preserved() {
245 245 </tbody>
246 246 </table>
247 247 </body>
248 - "#;
248 + ";
249 249
250 250 let md = convert(html);
251 251 assert!(md.contains("Order summary:"));
@@ -323,7 +323,7 @@ fn apple_mail_reply() {
323 323
324 324 #[test]
325 325 fn outlook_reply_with_separator() {
326 - let html = r#"
326 + let html = r"
327 327 <body>
328 328 <div>
329 329 <p>I'll handle it.</p>
@@ -339,7 +339,7 @@ fn outlook_reply_with_separator() {
339 339 <p>Can you take a look at the report?</p>
340 340 </div>
341 341 </body>
342 - "#;
342 + ";
343 343
344 344 let md = convert(html);
345 345 assert!(md.contains("I'll handle it."));