From eda287c13fe2eb0af362e7197df6ed7d1366a86a Mon Sep 17 00:00:00 2001 From: gospodima Date: Sun, 21 Sep 2025 13:33:10 +0200 Subject: [PATCH 1/3] chore: move release config to Cargo.toml, add clippy pedantic mode --- Cargo.toml | 11 +++++++++++ release.toml | 5 ----- 2 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 release.toml diff --git a/Cargo.toml b/Cargo.toml index b45c5b6..534798e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,14 @@ pathdiff = "0.2.3" pulldown-cmark = "0.13.0" regex = "1.11.2" walkdir = "2.5.0" + +[package.metadata.release] +pre-release-replacements = [ + { file = "pyproject.toml", search = 'version = "[a-z0-9\\.-]+"', replace = 'version = "{{version}}"' }, + { file = "README.md", search = 'rev: v[a-z0-9\\.-]+', replace = 'rev: v{{version}}' }, +] +pre-release-commit-message = "chore(release): prepare for {{version}}" + +[lints.clippy] +pedantic = "warn" + diff --git a/release.toml b/release.toml deleted file mode 100644 index a362c25..0000000 --- a/release.toml +++ /dev/null @@ -1,5 +0,0 @@ -pre-release-replacements = [ - {file="pyproject.toml", search='version = "[a-z0-9\\.-]+"', replace='version = "{{version}}"'}, - {file="README.md", search='rev: v[a-z0-9\\.-]+', replace='rev: v{{version}}'} -] -pre-release-commit-message = "chore(release): prepare for {{version}}" From fff5f3c85ad221e1cc9d2e627cae84aa9922effe Mon Sep 17 00:00:00 2001 From: gospodima Date: Sun, 21 Sep 2025 15:49:27 +0200 Subject: [PATCH 2/3] refactor: code changes for clippy warnings --- src/checks.rs | 2 +- src/checks/email.rs | 8 ++++---- src/checks/image.rs | 6 +++--- src/checks/section.rs | 28 +++++++++++++--------------- src/main.rs | 4 ++-- src/parser.rs | 6 +++++- src/scanner.rs | 21 +++++++++++++-------- src/utils.rs | 9 +++++++-- 8 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/checks.rs b/src/checks.rs index a0d3342..1ee1b4b 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -111,7 +111,7 @@ fn check_inline( validate_section_link(current_path, dest, doc_headings) } -fn to_exclude(dest: &str, exclude_link_regexes: &Vec) -> bool { +fn to_exclude(dest: &str, exclude_link_regexes: &[Regex]) -> bool { exclude_link_regexes .iter() .any(|re| re.is_match(dest.as_ref())) diff --git a/src/checks/email.rs b/src/checks/email.rs index 4a0195b..f66d750 100644 --- a/src/checks/email.rs +++ b/src/checks/email.rs @@ -1,14 +1,14 @@ use regex::Regex; pub fn validate_email(email: &str) -> Result<(), String> { - if !is_valid_email(email) { - Err(format!("Invalid email: {}", email)) - } else { + if is_valid_email(email) { Ok(()) + } else { + Err(format!("Invalid email: {email}")) } } -/// Email validation according to https://spec.commonmark.org/0.31.2/#email-address +/// Email validation according to fn is_valid_email(s: &str) -> bool { static EMAIL_RE: &str = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"; Regex::new(EMAIL_RE).unwrap().is_match(s) diff --git a/src/checks/image.rs b/src/checks/image.rs index de3c89f..85a5a03 100644 --- a/src/checks/image.rs +++ b/src/checks/image.rs @@ -10,9 +10,9 @@ pub fn validate_image(current_path: &Path, dest: &str) -> Result<(), String> { .unwrap_or_else(|| Path::new(".")) .join(dest); - if !resolved.exists() { - Err(format!("Image not found: {}", dest)) - } else { + if resolved.exists() { Ok(()) + } else { + Err(format!("Image not found: {dest}")) } } diff --git a/src/checks/section.rs b/src/checks/section.rs index 203be50..6b9d1ef 100644 --- a/src/checks/section.rs +++ b/src/checks/section.rs @@ -9,8 +9,7 @@ pub fn validate_section_link( ) -> Result<(), String> { let (file_part, heading_part) = dest .split_once('#') - .map(|(f, h)| (f, Some(h))) - .unwrap_or((dest, None)); + .map_or((dest, None), |(f, h)| (f, Some(h))); let target_file = if file_part.is_empty() { current_path.to_path_buf() @@ -20,24 +19,23 @@ pub fn validate_section_link( .unwrap_or_else(|| Path::new(".")) .join(file_part); fs::canonicalize(&resolved) - .map_err(|_| format!("File not found: {}", file_part))? + .map_err(|_| format!("File not found: {file_part}"))? }; - if let Some(heading) = heading_part { - if !section_links + if let Some(heading) = heading_part + && !section_links .entry(target_file.clone()) .or_insert_with(|| parser::parse_file_headings(&target_file).unwrap()) .contains(heading) - { - return Err(format!( - "Missing heading #{heading}{}", - if file_part.is_empty() { - "".to_string() - } else { - format!(" in {}", file_part) - } - )); - } + { + return Err(format!( + "Missing heading #{heading}{}", + if file_part.is_empty() { + String::new() + } else { + format!(" in {file_part}") + } + )); } Ok(()) diff --git a/src/main.rs b/src/main.rs index 006b6a6..986d092 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ fn main() { { let errors = run_checks(&content, path, &mut section_links, &config); for err in &errors { - println!("{}", err); + println!("{err}"); } if !errors.is_empty() { has_errors = true; @@ -35,5 +35,5 @@ fn main() { process::exit(1); } - println!("{}", "No broken references found.".green()) + println!("{}", "No broken references found.".green()); } diff --git a/src/parser.rs b/src/parser.rs index 3b650b2..16fa3fe 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -10,6 +10,8 @@ use crate::utils::create_options; pub type SectionLinkMap = HashMap>; /// Scan markdown file and collect section links based on its heading. +/// # Errors +/// This function will return an error if `path` does not already exist. pub fn parse_file_headings(path: &PathBuf) -> io::Result> { fs::read_to_string(path) .map(|content| crate::parser::collect_heading_links(&content)) @@ -34,6 +36,7 @@ pub fn parse_file_headings(path: &PathBuf) -> io::Result> { /// assert!(anchors.contains("intro-1")); /// assert!(anchors.contains("hello-world")); /// ``` +#[must_use] pub fn collect_heading_links(content: &str) -> HashSet { let mut headings = HashSet::new(); let mut heading_counter = HashMap::new(); @@ -53,7 +56,7 @@ pub fn collect_heading_links(content: &str) -> HashSet { Event::End(TagEnd::Heading { .. }) => { let base_link = heading2link(¤t_heading); let link = if let Some(counter) = heading_counter.get_mut(&base_link) { - let numbered_link = format!("{}-{}", base_link, counter); + let numbered_link = format!("{base_link}-{counter}"); *counter += 1; numbered_link } else { @@ -82,6 +85,7 @@ pub fn collect_heading_links(content: &str) -> HashSet { /// assert_eq!(heading2link("This -- Is__A_Test!"), "this----is__a_test"); /// assert_eq!(heading2link("A heading with 💡 emoji!"), "a-heading-with--emoji"); /// ``` +#[must_use] pub fn heading2link(text: &str) -> String { text.to_lowercase() .chars() diff --git a/src/scanner.rs b/src/scanner.rs index a4c934c..7f94fd7 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -9,15 +9,17 @@ use walkdir::WalkDir; use crate::utils::relative_path; /// Gather markdown files from paths (file or dir) -pub fn gather_markdown_files( +#[must_use] +pub fn gather_markdown_files( paths: &[PathBuf], - exclude: &HashSet, + exclude: &HashSet, ) -> Vec { paths .iter() - .flat_map(|path| match fs::canonicalize(path) { - Ok(canonical) => collect_markdown_from_path(&canonical, exclude), - Err(_) => { + .flat_map(|path| { + if let Ok(canonical) = fs::canonicalize(path) { + collect_markdown_from_path(&canonical, exclude) + } else { eprintln!( "{}", format!("Skipping invalid path: {}", path.display()).yellow() @@ -29,7 +31,10 @@ pub fn gather_markdown_files( } /// Collect markdown file(s) from a path (file or dir) -fn collect_markdown_from_path(path: &Path, exclude: &HashSet) -> Vec { +fn collect_markdown_from_path( + path: &Path, + exclude: &HashSet, +) -> Vec { if exclude.contains(path) { eprintln!( "{}", @@ -50,7 +55,7 @@ fn collect_markdown_from_path(path: &Path, exclude: &HashSet) -> Vec) -> Vec bool { - path.is_file() && path.extension().map_or(false, |ext| ext == "md") + path.is_file() && path.extension().is_some_and(|ext| ext == "md") } diff --git a/src/utils.rs b/src/utils.rs index 27b9c62..3d6c535 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,12 +6,14 @@ use std::{ use pulldown_cmark::Options; +#[must_use] pub fn create_options() -> Options { Options::ENABLE_FOOTNOTES | Options::ENABLE_WIKILINKS } -/// Create HashSet of canonicalized paths from vector of paths -pub fn create_file_set(vec_files: &Vec) -> HashSet { +/// Create ``HashSet`` of canonicalized paths from vector of paths +#[must_use] +pub fn create_file_set(vec_files: &[PathBuf]) -> HashSet { vec_files .iter() .filter_map(|s| fs::canonicalize(s).ok()) @@ -19,6 +21,7 @@ pub fn create_file_set(vec_files: &Vec) -> HashSet { } /// Return a path relative to the current working directory +#[must_use] pub fn relative_path(target: &Path) -> String { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); @@ -33,6 +36,7 @@ pub fn relative_path(target: &Path) -> String { } /// Return a Vec where each entry is the byte offset of the start of a line +#[must_use] pub fn compute_line_starts(text: &str) -> Vec { std::iter::once(0) .chain( @@ -43,6 +47,7 @@ pub fn compute_line_starts(text: &str) -> Vec { } /// Convert a byte offset into (line, column) given precomputed line starts +#[must_use] pub fn offset_to_line_col(offset: usize, line_starts: &[usize]) -> (usize, usize) { match line_starts.binary_search(&offset) { Ok(line) => (line + 1, 1), // exact match, first col From cf3254930ce3d615219d1e1dd417d7772cc357a3 Mon Sep 17 00:00:00 2001 From: gospodima Date: Mon, 22 Sep 2025 20:10:51 +0200 Subject: [PATCH 3/3] test: require_serial usage --- .pre-commit-hooks.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 8c17e83..42003c6 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,4 +4,5 @@ entry: mdrefcheck language: python types: [markdown] - args: [] \ No newline at end of file + require_serial: true + args: []