Skip to content

Commit 3672893

Browse files
authored
fix: enforce strict clippy lints and eliminate technical debt (#37)
- Add workspace-level clippy lints to deny unwrap_used, expect_used, panic - Configure test exemptions via cfg_attr(test, allow(...)) - Fix iTunes self-closing tag parsing bug (added is_empty parameter) - Refactor parse_channel() from 104 to 66 lines (extracted helpers) - Remove all TODO comments and phase mentions from code/docs - Update all test files with proper clippy allow attributes
1 parent 93a5649 commit 3672893

File tree

16 files changed

+148
-114
lines changed

16 files changed

+148
-114
lines changed

Cargo.lock

Lines changed: 35 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ nursery = { level = "warn", priority = -1 }
4848
module_name_repetitions = "allow"
4949
must_use_candidate = "allow"
5050
too_many_lines = "allow"
51+
unwrap_used = "deny"
52+
expect_used = "deny"
53+
panic = "deny"
5154

5255
[profile.release]
5356
lto = true

crates/feedparser-rs-core/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
2+
13
//! feedparser-rs-core: High-performance RSS/Atom/JSON Feed parser
24
//!
35
//! This crate provides a pure Rust implementation of feed parsing with
@@ -17,7 +19,6 @@
1719
//! </rss>
1820
//! "#;
1921
//!
20-
//! // Parsing will be fully implemented in Phase 2
2122
//! let feed = parse(xml.as_bytes()).unwrap();
2223
//! assert!(feed.bozo == false);
2324
//! ```

crates/feedparser-rs-core/src/parser/rss.rs

Lines changed: 91 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ fn parse_channel(
166166
feed.bozo_exception = Some(MALFORMED_ATTRIBUTES_ERROR.to_string());
167167
}
168168

169+
// Extract xml:lang before matching to avoid borrow issues
170+
let item_lang = extract_xml_lang(&e, limits.max_attribute_length);
171+
169172
// Use full qualified name to distinguish standard RSS tags from namespaced tags
170173
match tag.as_slice() {
171174
b"title" | b"link" | b"description" | b"language" | b"pubDate"
@@ -186,48 +189,21 @@ fn parse_channel(
186189
}
187190
}
188191
b"item" => {
189-
let item_lang = extract_xml_lang(&e, limits.max_attribute_length);
190-
191-
if !feed.check_entry_limit(reader, &mut buf, limits, depth)? {
192-
continue;
193-
}
194-
195-
let effective_lang = item_lang.as_deref().or(channel_lang);
196-
197-
match parse_item(reader, &mut buf, limits, depth, base_ctx, effective_lang)
198-
{
199-
Ok((entry, has_attr_errors)) => {
200-
if has_attr_errors {
201-
feed.bozo = true;
202-
feed.bozo_exception =
203-
Some(MALFORMED_ATTRIBUTES_ERROR.to_string());
204-
}
205-
feed.entries.push(entry);
206-
}
207-
Err(e) => {
208-
feed.bozo = true;
209-
feed.bozo_exception = Some(e.to_string());
210-
}
211-
}
192+
parse_channel_item(
193+
item_lang.as_deref(),
194+
reader,
195+
&mut buf,
196+
feed,
197+
limits,
198+
depth,
199+
base_ctx,
200+
channel_lang,
201+
)?;
212202
}
213203
_ => {
214-
let mut handled = parse_channel_itunes(
204+
parse_channel_extension(
215205
reader, &mut buf, &tag, &attrs, feed, limits, depth,
216206
)?;
217-
if !handled {
218-
handled = parse_channel_podcast(
219-
reader, &mut buf, &tag, &attrs, feed, limits,
220-
)?;
221-
}
222-
if !handled {
223-
handled = parse_channel_namespace(
224-
reader, &mut buf, &tag, feed, limits, *depth,
225-
)?;
226-
}
227-
228-
if !handled {
229-
skip_element(reader, &mut buf, limits, *depth)?;
230-
}
231207
}
232208
}
233209
*depth = depth.saturating_sub(1);
@@ -245,6 +221,71 @@ fn parse_channel(
245221
Ok(())
246222
}
247223

224+
/// Parse <item> element within channel
225+
///
226+
/// Note: Uses 8 parameters instead of a context struct due to borrow checker constraints
227+
/// with multiple simultaneous `&mut` references during parsing.
228+
#[inline]
229+
#[allow(clippy::too_many_arguments)]
230+
fn parse_channel_item(
231+
item_lang: Option<&str>,
232+
reader: &mut Reader<&[u8]>,
233+
buf: &mut Vec<u8>,
234+
feed: &mut ParsedFeed,
235+
limits: &ParserLimits,
236+
depth: &mut usize,
237+
base_ctx: &BaseUrlContext,
238+
channel_lang: Option<&str>,
239+
) -> Result<()> {
240+
if !feed.check_entry_limit(reader, buf, limits, depth)? {
241+
return Ok(());
242+
}
243+
244+
let effective_lang = item_lang.or(channel_lang);
245+
246+
match parse_item(reader, buf, limits, depth, base_ctx, effective_lang) {
247+
Ok((entry, has_attr_errors)) => {
248+
if has_attr_errors {
249+
feed.bozo = true;
250+
feed.bozo_exception = Some(MALFORMED_ATTRIBUTES_ERROR.to_string());
251+
}
252+
feed.entries.push(entry);
253+
}
254+
Err(e) => {
255+
feed.bozo = true;
256+
feed.bozo_exception = Some(e.to_string());
257+
}
258+
}
259+
260+
Ok(())
261+
}
262+
263+
/// Parse channel extension elements (iTunes, Podcast, namespaces)
264+
#[inline]
265+
fn parse_channel_extension(
266+
reader: &mut Reader<&[u8]>,
267+
buf: &mut Vec<u8>,
268+
tag: &[u8],
269+
attrs: &[(Vec<u8>, String)],
270+
feed: &mut ParsedFeed,
271+
limits: &ParserLimits,
272+
depth: &mut usize,
273+
) -> Result<()> {
274+
let mut handled = parse_channel_itunes(reader, buf, tag, attrs, feed, limits, depth)?;
275+
if !handled {
276+
handled = parse_channel_podcast(reader, buf, tag, attrs, feed, limits)?;
277+
}
278+
if !handled {
279+
handled = parse_channel_namespace(reader, buf, tag, feed, limits, *depth)?;
280+
}
281+
282+
if !handled {
283+
skip_element(reader, buf, limits, *depth)?;
284+
}
285+
286+
Ok(())
287+
}
288+
248289
/// Parse enclosure element from attributes
249290
#[inline]
250291
fn parse_enclosure(attrs: &[(Vec<u8>, String)], limits: &ParserLimits) -> Option<Enclosure> {
@@ -621,8 +662,9 @@ fn parse_item(
621662
}
622663
}
623664
_ => {
624-
let mut handled =
625-
parse_item_itunes(reader, buf, &tag, &attrs, &mut entry, limits)?;
665+
let mut handled = parse_item_itunes(
666+
reader, buf, &tag, &attrs, &mut entry, limits, is_empty, *depth,
667+
)?;
626668
if !handled {
627669
handled = parse_item_podcast(
628670
reader, buf, &tag, &attrs, &mut entry, limits, is_empty, *depth,
@@ -729,14 +771,20 @@ fn parse_item_standard(
729771
/// Parse iTunes namespace tags at item level
730772
///
731773
/// Returns `Ok(true)` if the tag was recognized and handled, `Ok(false)` if not recognized.
774+
///
775+
/// Note: Uses 8 parameters instead of a context struct due to borrow checker constraints
776+
/// with multiple simultaneous `&mut` references during parsing.
732777
#[inline]
778+
#[allow(clippy::too_many_arguments)]
733779
fn parse_item_itunes(
734780
reader: &mut Reader<&[u8]>,
735781
buf: &mut Vec<u8>,
736782
tag: &[u8],
737783
attrs: &[(Vec<u8>, String)],
738784
entry: &mut Entry,
739785
limits: &ParserLimits,
786+
is_empty: bool,
787+
depth: usize,
740788
) -> Result<bool> {
741789
if is_itunes_tag(tag, b"title") {
742790
let text = read_text(reader, buf, limits)?;
@@ -763,6 +811,9 @@ fn parse_item_itunes(
763811
let itunes = entry.itunes.get_or_insert_with(ItunesEntryMeta::default);
764812
itunes.image = Some(truncate_to_length(value, limits.max_attribute_length));
765813
}
814+
if !is_empty {
815+
skip_element(reader, buf, limits, depth)?;
816+
}
766817
Ok(true)
767818
} else if is_itunes_tag(tag, b"episode") {
768819
let text = read_text(reader, buf, limits)?;

0 commit comments

Comments
 (0)