Skip to content

Commit 677d3a5

Browse files
ekropotinclaude
andauthored
feat: implement MD053 link-image-reference-definitions rule (#28)
Add comprehensive MD053 rule to detect unused and duplicate link/image reference definitions. Features: - Detects unused reference definitions - Identifies duplicate definitions (first definition wins per CommonMark) - Supports all reference formats: full [text][label], collapsed [label][], shortcut [label] - CommonMark compliant label normalization (case-insensitive, whitespace collapse) - Configurable ignored definitions with sensible defaults (["//"] for comments) - Performance optimized with pre-compiled regex patterns and HashMap lookups Implementation: - Document-wide rule type for comprehensive analysis - 13 comprehensive unit tests covering all scenarios and edge cases - Proper integration with configuration system and CLI - Complete documentation and test sample files - Code formatting improvements across multiple files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8f1a03d commit 677d3a5

22 files changed

Lines changed: 1455 additions & 447 deletions

File tree

.claude/commands/review.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
Tidy up the code by deleting old, unused code and fixing compiler hints/warnings. Suppress false positive ones, if needed.
2-
When do general code review, check adherence to the best coding practices on Rust. Also, pay a special attention on potential performance/resource overuse issues.
1+
Review the code changes in the current branch, ensuring they adhere to the coding standards outlined in CLAUDE.md.

CLAUDE.md

Lines changed: 54 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -121,99 +121,6 @@ quickmark/
121121
- Each rule implements `RuleLinter` trait with `feed` method
122122
- Rules are dynamically instantiated based on configuration
123123

124-
### Rules Implementation Checklist
125-
126-
This section tracks the progress of porting all markdownlint rules to QuickMark. Rules are categorized by their implementation requirements:
127-
128-
#### Line-Based Rules (5 rules)
129-
130-
*Work primarily with raw text lines - high performance, direct text analysis*
131-
132-
- [ ] **MD009** (`no-trailing-spaces`): Trailing spaces at end of lines
133-
- [ ] **MD010** (`no-hard-tabs`): Hard tabs should not be used
134-
- [ ] **MD012** (`no-multiple-blanks`): Multiple consecutive blank lines
135-
- [x] **MD013** (`line-length`): Line length limits with configurable exceptions ✅
136-
- [ ] **MD047** (`single-trailing-newline`): Files should end with a single newline
137-
138-
#### Token-Based Rules (32 rules)
139-
140-
*Work with specific AST node types - cached node filtering for efficiency*
141-
142-
**Heading Rules (8 rules):**
143-
144-
- [x] **MD001** (`heading-increment`): Heading levels increment by one ✅
145-
- [x] **MD003** (`heading-style`): Consistent heading styles ✅
146-
- [ ] **MD018** (`no-missing-space-atx`): Space after hash in ATX headings
147-
- [ ] **MD019** (`no-multiple-space-atx`): Multiple spaces after hash in ATX headings
148-
- [ ] **MD020** (`no-missing-space-closed-atx`): Space inside closed ATX headings
149-
- [ ] **MD021** (`no-multiple-space-closed-atx`): Multiple spaces in closed ATX headings
150-
- [ ] **MD023** (`heading-start-left`): Headings start at beginning of line
151-
- [ ] **MD026** (`no-trailing-punctuation`): Trailing punctuation in headings
152-
153-
**List Rules (6 rules):**
154-
155-
- [ ] **MD004** (`ul-style`): Unordered list style consistency
156-
- [ ] **MD005** (`list-indent`): List item indentation at same level
157-
- [ ] **MD006** (`ul-start-left`): Bulleted lists start at beginning of line
158-
- [ ] **MD007** (`ul-indent`): Unordered list indentation consistency
159-
- [ ] **MD029** (`ol-prefix`): Ordered list item prefix consistency
160-
- [ ] **MD030** (`list-marker-space`): Spaces after list markers
161-
162-
**Link Rules (3 rules):**
163-
164-
- [ ] **MD011** (`no-reversed-links`): Reversed link syntax
165-
- [ ] **MD034** (`no-bare-urls`): Bare URLs without proper formatting
166-
- [ ] **MD042** (`no-empty-links`): Empty links
167-
168-
**Code Rules (4 rules):**
169-
170-
- [ ] **MD014** (`commands-show-output`): Dollar signs before shell commands
171-
- [ ] **MD040** (`fenced-code-language`): Language specified for fenced code blocks
172-
- [ ] **MD046** (`code-block-style`): Code block style consistency
173-
- [ ] **MD048** (`code-fence-style`): Code fence style consistency
174-
175-
**Formatting Rules (11 rules):**
176-
177-
- [ ] **MD027** (`no-multiple-space-blockquote`): Multiple spaces after blockquote
178-
- [ ] **MD028** (`no-blanks-blockquote`): Blank lines inside blockquotes
179-
- [ ] **MD033** (`no-inline-html`): Inline HTML usage
180-
- [ ] **MD035** (`hr-style`): Horizontal rule style consistency
181-
- [ ] **MD036** (`no-emphasis-as-heading`): Emphasis used instead of heading
182-
- [ ] **MD037** (`no-space-in-emphasis`): Spaces inside emphasis markers
183-
- [ ] **MD038** (`no-space-in-code`): Spaces inside code span elements
184-
- [ ] **MD039** (`no-space-in-links`): Spaces inside link text
185-
- [ ] **MD045** (`no-alt-text`): Images should have alternate text
186-
- [ ] **MD049** (`emphasis-style`): Emphasis style consistency
187-
- [ ] **MD050** (`strong-style`): Strong style consistency
188-
189-
#### Document-Wide Rules (7 rules)
190-
191-
*Require full document analysis - global state tracking*
192-
193-
- [ ] **MD024** (`no-duplicate-heading`): Multiple headings with same content
194-
- [ ] **MD025** (`single-title`): Multiple top-level headings
195-
- [ ] **MD041** (`first-line-heading`): First line should be top-level heading
196-
- [ ] **MD043** (`required-headings`): Required heading structure
197-
- [x] **MD051** (`link-fragments`): Link fragments should be valid
198-
- [x] **MD052** (`reference-links-images`): Reference links should be defined ✅
199-
- [ ] **MD053** (`link-image-reference-definitions`): Reference definitions should be needed
200-
201-
#### Hybrid Rules (3 rules)
202-
203-
*Need both AST analysis and line context - structural elements with spacing*
204-
205-
- [ ] **MD022** (`blanks-around-headings`): Headings surrounded by blank lines
206-
- [ ] **MD031** (`blanks-around-fences`): Fenced code blocks surrounded by blank lines
207-
- [ ] **MD032** (`blanks-around-lists`): Lists surrounded by blank lines
208-
209-
#### Special Rules (1 rule)
210-
211-
*Unique implementation requirements*
212-
213-
- [ ] **MD044** (`proper-names`): Proper names with correct capitalization (requires external dictionaries)
214-
215-
**Implementation Progress: 4/48 rules completed (8.33%)**
216-
217124
### Linting Architecture Evolution
218125

219126
**Performance-Optimized Single-Pass Design**:
@@ -314,3 +221,57 @@ This architecture allows rules like MD013 to work efficiently with raw text whil
314221
1. Create conversion functions in `quickmark_config`
315222
2. Add new public functions following the pattern of `parse_toml_config`
316223
3. Both CLI and server applications can immediately use the new format
224+
225+
## Code Guidelines
226+
227+
### General principles
228+
229+
1. **Follow idiomatic Rust**: Use the latest Rust best practices and conventions. Code should feel natural to an experienced Rust developer.
230+
2. **No unsafe unless required**: Do not use unsafe blocks unless absolutely necessary for performance or interoperability. If used, justify with a comment and encapsulate safely.
231+
3. **Zero compiler warnings**:
232+
- Code must compile with `#![deny(warnings)]`.
233+
- Suppress only known false positives with clearly scoped `#[allow(...)]` attributes and documented reasons.
234+
4. **Speed over memory**:
235+
- Optimize for CPU performance, even at the cost of increased memory usage.
236+
- Avoid unnecessary allocations, but favor speed in algorithms and data access patterns.
237+
5. **Cloning is expensive**: Avoid cloning (`.clone()`) unless it is proven to be more efficient than passing a reference or performing in-place mutation.
238+
6. **Use modern language features**:
239+
- Prefer `let else`, `if let`, match ergonomics, Iterator combinators, `?` operator, and Result-based error handling.
240+
- Consider `Cow` or `Arc` where applicable to avoid unnecessary clones.
241+
242+
### Optimization practices
243+
244+
1. **Prefer move semantics** when data is no longer needed.
245+
2. **Use references wisely**: Use `&T` or `&mut T` rather than `T` or `T.clone()` when ownership is not required.
246+
3. **Inline strategically**: Inline functions where beneficial using `#[inline]` (but measure if in doubt).
247+
4. **Use zero-cost abstractions**: From the standard library or crates like `itertools`, `smallvec`, `rayon` (for parallelism), etc.
248+
5. **Choose fast data structures**: For hot paths, prefer faster data structures, even if more memory is consumed (e.g., `HashMap` over `BTreeMap` when ordering is unnecessary).
249+
250+
### Safety and correctness
251+
252+
1. **Use strong typing**: Use the type system to enforce invariants.
253+
2. **Avoid panics**: In library code (`unwrap()`, `expect()`) unless in clearly unreachable branches.
254+
3. **Mark important results**: Use `#[must_use]` to mark important results when appropriate.
255+
4. **Document assumptions**: Document all `TODO`, `FIXME`, and assumptions in code.
256+
257+
### Linting & Tooling
258+
259+
**Code must pass**:
260+
261+
1. `cargo check`
262+
2. `cargo clippy --all-targets --all-features -- -D warnings`
263+
3. `cargo fmt --check`
264+
4. `cargo test --all`
265+
266+
**Additional practices**:
267+
268+
1. **Use Clippy lints**: That enforce performance best practices (e.g., `clippy::redundant_clone`, `clippy::needless_collect`, `clippy::manual_memcpy`, etc.)
269+
2. **Use performance attributes**: `#[inline(always)]`, `#[cold]`, or `#[no_mangle]` where profiling/FFI suggests it makes sense — but only after benchmarking.
270+
271+
### Testing & Validation
272+
273+
**Tests must**:
274+
275+
1. **Cover edge cases**: And performance regressions.
276+
2. **Use `#[should_panic]`**: Where panics are expected.
277+
3. **Prefer property-based testing**: Use `proptest` or fuzzing where inputs are highly variable.

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ heading-style = 'err'
5252
line-length = 'err'
5353
link-fragments = 'warn'
5454
reference-links-images = 'err'
55+
link-image-reference-definitions = 'err'
5556

5657
# see a specific rule's doc for details of configuration
5758
[linters.settings.heading-style]
@@ -72,11 +73,14 @@ ignored_pattern = ""
7273
[linters.settings.reference-links-images]
7374
shortcut_syntax = false
7475
ignored_labels = ["x"]
76+
77+
[linters.settings.link-image-reference-definitions]
78+
ignored_definitions = ["//"]
7579
```
7680

7781
## Rules
7882

79-
**Implementation Progress: 5/48 rules completed (10.4%)**
83+
**Implementation Progress: 6/48 rules completed (12.5%)**
8084

8185
- [x] **[MD001](docs/rules/md001.md)** *heading-increment* - Heading levels should only increment by one level at a time
8286
- [x] **[MD003](docs/rules/md003.md)** *heading-style* - Consistent heading styles
@@ -125,4 +129,4 @@ ignored_labels = ["x"]
125129
- [ ] **MD050** *strong-style* - Strong style consistency
126130
- [x] **[MD051](docs/rules/md051.md)** *link-fragments* - Link fragments should be valid
127131
- [x] **[MD052](docs/rules/md052.md)** *reference-links-images* - Reference links should be defined
128-
- [ ] **MD053** *link-image-reference-definitions* - Reference definitions should be needed
132+
- [x] **[MD053](docs/rules/md053.md)** *link-image-reference-definitions* - Reference definitions should be needed

crates/quickmark/src/main.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use anyhow::Context;
22
use clap::Parser;
33
use quickmark_config::config_in_path_or_default;
4-
use quickmark_linter::linter::{MultiRuleLinter, RuleViolation};
54
use quickmark_linter::config::{QuickmarkConfig, RuleSeverity};
5+
use quickmark_linter::linter::{MultiRuleLinter, RuleViolation};
66
use std::cmp::min;
77
use std::env;
88
use std::{fs, path::PathBuf, process::exit};
@@ -73,11 +73,11 @@ fn main() -> anyhow::Result<()> {
7373
#[cfg(test)]
7474
mod tests {
7575
use super::*;
76-
use std::path::PathBuf;
7776
use quickmark_linter::config::{HeadingStyle, LintersSettingsTable, MD003HeadingStyleTable};
7877
use quickmark_linter::linter::{CharPosition, Range};
7978
use quickmark_linter::rules::{md001::MD001, md003::MD003};
8079
use quickmark_linter::test_utils::test_helpers::test_config_with_settings;
80+
use std::path::PathBuf;
8181

8282
#[test]
8383
fn test_print_cli_errors() {
@@ -94,14 +94,35 @@ mod tests {
9494
},
9595
);
9696
let range = Range {
97-
start: CharPosition { line: 1, character: 1 },
98-
end: CharPosition { line: 1, character: 5 },
97+
start: CharPosition {
98+
line: 1,
99+
character: 1,
100+
},
101+
end: CharPosition {
102+
line: 1,
103+
character: 5,
104+
},
99105
};
100106
let file = PathBuf::default();
101107
let results = vec![
102-
RuleViolation::new(&MD001, "all is bad".to_string(), file.clone(), range.clone()),
103-
RuleViolation::new(&MD003, "all is even worse".to_string(), file.clone(), range.clone()),
104-
RuleViolation::new(&MD003, "all is even worse2".to_string(), file.clone(), range),
108+
RuleViolation::new(
109+
&MD001,
110+
"all is bad".to_string(),
111+
file.clone(),
112+
range.clone(),
113+
),
114+
RuleViolation::new(
115+
&MD003,
116+
"all is even worse".to_string(),
117+
file.clone(),
118+
range.clone(),
119+
),
120+
RuleViolation::new(
121+
&MD003,
122+
"all is even worse2".to_string(),
123+
file.clone(),
124+
range,
125+
),
105126
];
106127

107128
let (errs, warns) = print_cli_errors(&results, &config);

crates/quickmark/tests/cli_integration_tests.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ use std::path::PathBuf;
77
/// Helper function to get the path to test sample files
88
fn test_sample_path(filename: &str) -> String {
99
// Use the CARGO_MANIFEST_DIR environment variable to find the project root
10-
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
11-
.expect("CARGO_MANIFEST_DIR not set");
10+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
1211

1312
PathBuf::from(manifest_dir)
1413
.parent() // Go up from crates/quickmark
@@ -105,7 +104,8 @@ fn test_cli_error_format() {
105104

106105
// Check that error format includes expected components:
107106
// ERR: file_path:line:column MD001/heading-increment message
108-
let error_lines: Vec<&str> = stderr.lines()
107+
let error_lines: Vec<&str> = stderr
108+
.lines()
109109
.filter(|line| line.starts_with("ERR:") || line.starts_with("WARN:"))
110110
.collect();
111111

@@ -162,7 +162,8 @@ fn test_cli_line_numbers_are_one_based() {
162162
let stderr = String::from_utf8_lossy(&output.stderr);
163163

164164
// Find error lines and check line numbers
165-
let error_lines: Vec<&str> = stderr.lines()
165+
let error_lines: Vec<&str> = stderr
166+
.lines()
166167
.filter(|line| line.starts_with("ERR:") || line.starts_with("WARN:"))
167168
.collect();
168169

@@ -174,7 +175,10 @@ fn test_cli_line_numbers_are_one_based() {
174175
let line_num_str = &after_file[..second_colon];
175176
if let Ok(line_num) = line_num_str.parse::<u32>() {
176177
// Line numbers should be 1-based, not 0-based
177-
assert!(line_num >= 1, "Line number should be 1-based, got: {line_num}");
178+
assert!(
179+
line_num >= 1,
180+
"Line number should be 1-based, got: {line_num}"
181+
);
178182
}
179183
}
180184
}

0 commit comments

Comments
 (0)