1use log::debug;
2use std::path::Path;
3use std::{marker::PhantomData, path::PathBuf};
4
5use crate::todo_extractor_internal::languages::common::CommentParser;
6use crate::todo_extractor_internal::languages::common_syntax;
7use log::{error, info};
8use pest::Parser;
9
10#[derive(Debug, PartialEq, Clone, Eq)]
12pub struct MarkedItem {
13 pub file_path: PathBuf,
14 pub line_number: usize,
15 pub message: String,
16 pub marker: String,
17}
18
19pub struct MarkerConfig {
21 pub markers: Vec<String>,
22}
23
24impl MarkerConfig {
25 pub fn normalized(markers: Vec<String>) -> Self {
27 let markers = markers
28 .into_iter()
29 .map(|m| m.trim().trim_end_matches(':').trim().to_string())
30 .collect();
31 MarkerConfig { markers }
32 }
33}
34
35impl Default for MarkerConfig {
36 fn default() -> Self {
37 MarkerConfig {
38 markers: vec!["TODO".to_string()],
39 }
40 }
41}
42
43pub fn parse_comments<P: Parser<R>, R: pest::RuleType>(
50 _parser_type: PhantomData<P>,
51 rule: R,
52 file_content: &str,
53) -> Vec<CommentLine> {
54 let parse_result = P::parse(rule, file_content);
55 let mut comments = Vec::new();
56
57 match parse_result {
58 Ok(pairs) => {
59 debug!(
60 "Parsing successful! Found {} top-level pairs.",
61 pairs.clone().count()
62 );
63
64 for pair in pairs {
65 for inner_pair in pair.into_inner() {
67 if let Some(comment) = extract_comment_from_pair(inner_pair) {
74 debug!("Extracted comment: {comment:?}",);
75 comments.push(comment);
76 } else {
77 }
79 }
80 }
81 }
82 Err(e) => {
83 error!("Parsing error: {e:?}");
84 }
85 }
86
87 comments
88}
89
90fn extract_comment_from_pair(
95 pair: pest::iterators::Pair<impl pest::RuleType>,
96) -> Option<CommentLine> {
97 let span = pair.as_span();
98 let base_line = span.start_pos().line_col().0; let text = span.as_str().trim(); let rule_name = format!("{:?}", pair.as_rule()).to_lowercase();
102 if rule_name.contains("non_comment") {
104 return None;
105 }
106 if (rule_name.contains("comment") || rule_name.contains("docstring")) && !text.is_empty() {
108 Some(CommentLine {
109 line_number: base_line,
110 text: text.to_string(),
111 })
112 } else {
113 None
114 }
115}
116
117fn split_multiline_comment_line(line: &CommentLine) -> Vec<CommentLine> {
122 let mut result = Vec::new();
123 for (i, part) in line.text.split('\n').enumerate() {
125 result.push(CommentLine {
128 line_number: line.line_number + i,
129 text: part.to_string(),
130 });
131 }
132 result
133}
134
135fn flatten_comment_lines(lines: &[CommentLine]) -> Vec<CommentLine> {
141 let mut flattened = Vec::new();
142 for line in lines {
143 if line.text.contains('\n') {
144 flattened.extend(split_multiline_comment_line(line));
145 } else {
146 flattened.push(line.clone());
147 }
148 }
149 flattened
150}
151
152pub fn get_effective_extension(path: &Path) -> String {
157 let extension = path
158 .extension()
159 .and_then(|s| s.to_str())
160 .unwrap_or("")
161 .to_lowercase();
162
163 let file_name = path
165 .file_name()
166 .and_then(|s| s.to_str())
167 .unwrap_or("")
168 .to_lowercase();
169
170 if extension.is_empty() && file_name == "dockerfile" {
171 "dockerfile".to_string()
172 } else {
173 extension
174 }
175}
176
177pub fn get_parser_for_extension(
182 extension: &str,
183 file_path: &Path,
184) -> Option<fn(&str) -> Vec<CommentLine>> {
185 let result: Option<fn(&str) -> Vec<CommentLine>> = match extension {
186 "py" => {
188 Some(crate::todo_extractor_internal::languages::python::PythonParser::parse_comments)
189 }
190
191 "rs" => Some(crate::todo_extractor_internal::languages::rust::RustParser::parse_comments),
193
194 "js" | "jsx" | "mjs" => {
196 Some(crate::todo_extractor_internal::languages::js::JsParser::parse_comments)
197 }
198
199 "ts" | "tsx" | "java" | "cpp" | "hpp" | "cc" | "hh" | "cs" | "swift" | "kt" | "kts"
201 | "json" => Some(crate::todo_extractor_internal::languages::js::JsParser::parse_comments),
202
203 "go" => Some(crate::todo_extractor_internal::languages::go::GoParser::parse_comments),
205
206 "sh" => Some(crate::todo_extractor_internal::languages::shell::ShellParser::parse_comments),
208 "toml" => Some(crate::todo_extractor_internal::languages::toml::TomlParser::parse_comments),
209 "dockerfile" => Some(
210 crate::todo_extractor_internal::languages::dockerfile::DockerfileParser::parse_comments,
211 ),
212
213 "yml" | "yaml" => {
215 Some(crate::todo_extractor_internal::languages::yaml::YamlParser::parse_comments)
216 }
217
218 "sql" => Some(crate::todo_extractor_internal::languages::sql::SqlParser::parse_comments),
220
221 "md" => Some(
223 crate::todo_extractor_internal::languages::markdown::MarkdownParser::parse_comments,
224 ),
225
226 _ => None,
227 };
228
229 match &result {
231 Some(_) => {
232 info!("file {:?} have a valid parser", file_path);
233 }
234 None => {
235 debug!(
236 "No parser found for extension '{}' in file: {:?}",
237 extension, file_path
238 );
239 }
240 }
241
242 result
243}
244
245pub fn extract_marked_items_with_parser(
247 path: &Path,
248 file_content: &str,
249 parser_fn: fn(&str) -> Vec<CommentLine>,
250 config: &MarkerConfig,
251) -> Vec<MarkedItem> {
252 debug!("extract_marked_items_with_parser for file {path:?}");
253
254 let comment_lines = parser_fn(file_content);
255
256 debug!(
257 "extract_marked_items_with_parser: found {} comment lines from parser: {:?}",
258 comment_lines.len(),
259 comment_lines
260 );
261
262 let marked_items = collect_marked_items_from_comment_lines(&comment_lines, config, path);
264 debug!(
265 "extract_marked_items_with_parser: found {} marked items total",
266 marked_items.len()
267 );
268 marked_items
269}
270
271pub fn extract_marked_items_from_file(
272 file: &Path,
273 marker_config: &MarkerConfig,
274) -> Result<Vec<MarkedItem>, String> {
275 let effective_ext = get_effective_extension(file);
276 let parser_fn = match get_parser_for_extension(&effective_ext, file) {
277 Some(parser) => parser,
278 None => {
279 info!("Skipping unsupported file type: {:?}", file);
281 return Ok(Vec::new());
282 }
283 };
284
285 match std::fs::read_to_string(file) {
286 Ok(content) => {
287 let todos = extract_marked_items_with_parser(file, &content, parser_fn, marker_config);
288 Ok(todos)
289 }
290 Err(e) => {
291 error!("Warning: Could not read file {file:?}, skipping. Error: {e}");
292 Err(format!("Could not read file {:?}: {}", file, e))
293 }
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct CommentLine {
300 pub line_number: usize,
301 pub text: String,
302}
303
304pub fn collect_marked_items_from_comment_lines(
308 lines: &[CommentLine],
309 config: &MarkerConfig,
310 path: &Path,
311) -> Vec<MarkedItem> {
312 let stripped_lines = strip_and_flatten(lines);
314 let blocks = group_lines_into_blocks_with_marker(stripped_lines, &config.markers);
316 blocks
318 .into_iter()
319 .map(|(line_number, marker, block)| MarkedItem {
320 file_path: path.to_path_buf(),
321 line_number,
322 message: process_block_lines(&block, &config.markers),
323 marker,
324 })
325 .collect()
326}
327
328fn strip_and_flatten(lines: &[CommentLine]) -> Vec<CommentLine> {
330 flatten_comment_lines(lines)
331 .into_iter()
332 .map(|cl| CommentLine {
333 line_number: cl.line_number,
334 text: common_syntax::strip_markers(&cl.text),
335 })
336 .collect()
337}
338
339fn group_lines_into_blocks_with_marker(
344 lines: Vec<CommentLine>,
345 markers: &[String],
346) -> Vec<(usize, String, Vec<String>)> {
347 let mut blocks = Vec::new();
348 let mut current_block: Option<(usize, String, Vec<String>)> = None;
349
350 for cl in lines {
351 let trimmed = cl.text.trim().to_string();
352 let matched_marker = markers.iter().find_map(|base| {
356 if let Some(rest) = trimmed.strip_prefix(base) {
357 if rest.is_empty() || rest.starts_with(' ') || rest.starts_with(':') {
358 return Some(base.clone());
359 }
360 }
361 None
362 });
363 if let Some(marker) = matched_marker {
364 if let Some(block) = current_block.take() {
366 blocks.push(block);
367 }
368 current_block = Some((cl.line_number, marker, vec![trimmed]));
370 } else if let Some((_, _, ref mut block_lines)) = current_block {
371 if cl.text.starts_with(' ') || cl.text.starts_with('\t') {
373 block_lines.push(trimmed);
374 } else {
375 blocks.push(current_block.take().unwrap());
377 }
378 }
379 }
381
382 if let Some(block) = current_block {
384 blocks.push(block);
385 }
386 blocks
387}
388
389fn process_block_lines(lines: &[String], markers: &[String]) -> String {
398 let merged = lines.join(" ");
399 markers.iter().fold(merged, |acc, marker| {
400 if let Some(stripped) = acc.strip_prefix(marker) {
401 let stripped = if let Some(rest) = stripped.strip_prefix(":") {
403 rest
404 } else {
405 stripped
406 };
407 stripped.trim().to_string()
408 } else {
409 acc
410 }
411 })
412}
413
414#[cfg(test)]
415mod aggregator_tests {
416 use super::*;
417 use crate::test_utils::{init_logger, test_extract_marked_items};
418
419 #[test]
420 fn test_valid_rust_extension() {
421 init_logger();
422 let src = "// TODO: Implement feature X";
423 let config = MarkerConfig {
424 markers: vec!["TODO:".to_string()],
425 };
426 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
427 assert_eq!(todos.len(), 1);
428 assert_eq!(todos[0].marker, "TODO:");
429 }
430
431 #[test]
432 fn test_valid_js_extension() {
433 init_logger();
434 let src = "// TODO: Implement feature X";
435 let config = MarkerConfig {
436 markers: vec!["TODO:".to_string()],
437 };
438 let todos = test_extract_marked_items(Path::new("file.js"), src, &config);
439 assert_eq!(todos.len(), 1);
440 assert_eq!(todos[0].marker, "TODO:");
441 }
442
443 #[test]
444 fn test_valid_jsx_extension() {
445 init_logger();
446 let src = "// TODO: Add prop validation";
447 let config = MarkerConfig {
448 markers: vec!["TODO:".to_string()],
449 };
450 let todos = test_extract_marked_items(Path::new("component.jsx"), src, &config);
451 assert_eq!(todos.len(), 1);
452 assert_eq!(todos[0].marker, "TODO:");
453 }
454
455 #[test]
456 fn test_valid_go_extension() {
457 init_logger();
458 let src = "// TODO: Implement feature X";
459 let config = MarkerConfig {
460 markers: vec!["TODO:".to_string()],
461 };
462 let todos = test_extract_marked_items(Path::new("main.go"), src, &config);
463 assert_eq!(todos.len(), 1);
464 assert_eq!(todos[0].marker, "TODO:");
465 }
466
467 #[test]
468 fn test_invalid_extension() {
469 init_logger();
470 let src = "// TODO: This should not be processed";
471 let config = MarkerConfig {
472 markers: vec!["TODO:".to_string()],
473 };
474 let todos = test_extract_marked_items(Path::new("file.unknown"), src, &config);
475 assert_eq!(todos.len(), 0);
476 }
477
478 #[test]
479 fn test_merge_multiline_todo() {
480 init_logger();
481 let src = r#"
482// TODO: Fix bug
483// Improve error handling
484// Add logging
485"#;
486 let config = MarkerConfig {
487 markers: vec!["TODO:".to_string()],
488 };
489 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
490 assert_eq!(todos.len(), 1);
491 assert_eq!(
492 todos[0].message,
493 "Fix bug Improve error handling Add logging"
494 );
495 assert_eq!(todos[0].marker, "TODO:");
496 }
497
498 #[test]
499 fn test_stop_merge_on_unindented_line() {
500 init_logger();
501 let src = r#"
502// TODO: Improve API
503// Refactor later
504"#;
505 let config = MarkerConfig {
506 markers: vec!["TODO:".to_string()],
507 };
508 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
509 assert_eq!(todos.len(), 1);
510 assert_eq!(todos[0].message, "Improve API"); }
512
513 #[test]
514 fn test_todo_with_line_number() {
515 init_logger();
516 let src = r#"
517// Some comment
518// TODO: Implement caching
519"#;
520 let config = MarkerConfig {
521 markers: vec!["TODO:".to_string()],
522 };
523 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
524 assert_eq!(todos.len(), 1);
525 assert_eq!(todos[0].line_number, 3);
526 assert_eq!(todos[0].message, "Implement caching");
527 }
528
529 #[test]
530 fn test_empty_input_no_todos() {
531 init_logger();
532 let src = "";
533 let config = MarkerConfig {
534 markers: vec!["TODO:".to_string()],
535 };
536 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
537 assert_eq!(todos.len(), 0);
538 }
539
540 #[test]
541 fn test_display_todo_output() {
542 init_logger();
543 let src = "// TODO: Improve logging";
544 let config = MarkerConfig {
545 markers: vec!["TODO:".to_string()],
546 };
547 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
548
549 let output = format!("{} - {}", todos[0].line_number, todos[0].message);
550 assert_eq!(output, "1 - Improve logging");
551 }
552
553 #[test]
554 fn test_display_no_todos() {
555 init_logger();
556 let src = "fn main() {}";
557 let config = MarkerConfig {
558 markers: vec!["TODO:".to_string()],
559 };
560 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
561 assert!(todos.is_empty());
562 }
563
564 #[test]
565 fn test_basic_framework() {
566 init_logger();
567 assert_eq!(2 + 2, 4);
568 }
569
570 #[test]
571 fn test_false_positive_detection() {
572 init_logger();
573 let src = r#"
574let message = "TODO: This should not be detected";
575"#;
576 let config = MarkerConfig {
577 markers: vec!["TODO:".to_string()],
578 };
579 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
580 assert_eq!(todos.len(), 0);
581 }
582
583 #[test]
584 fn test_multiple_consecutive_todos() {
585 init_logger();
586 let src = r#"
587// TODO: todo1
588// TODO: todo2
589"#;
590 let config = MarkerConfig {
591 markers: vec!["TODO:".to_string()],
592 };
593 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
594
595 assert_eq!(todos.len(), 2);
596
597 assert_eq!(todos[0].line_number, 2);
600 assert_eq!(todos[0].message, "todo1");
601 assert_eq!(todos[1].line_number, 3);
602 assert_eq!(todos[1].message, "todo2");
603 }
604
605 #[test]
606 fn test_mixed_marker_configurations() {
607 let src = r#"
609// TODO: Implement feature
610// FIXME Fix bug
611// TODO Add docs
612// FIXME: Refactor
613"#;
614 let config = MarkerConfig {
615 markers: vec!["TODO".to_string(), "FIXME".to_string()],
616 };
617 let items = test_extract_marked_items(Path::new("file.rs"), src, &config);
618 assert_eq!(items.len(), 4);
619 assert_eq!(items[0].marker, "TODO");
620 assert_eq!(items[0].message, "Implement feature");
621 assert_eq!(items[1].marker, "FIXME");
622 assert_eq!(items[1].message, "Fix bug");
623 assert_eq!(items[2].marker, "TODO");
624 assert_eq!(items[2].message, "Add docs");
625 assert_eq!(items[3].marker, "FIXME");
626 assert_eq!(items[3].message, "Refactor");
627 }
628
629 #[test]
630 fn test_ignore_todo_not_at_beginning() {
631 let src = r#"
632// This is a comment with a TODO: not at the beginning
633fn main() {}
634"#;
635 let config = MarkerConfig {
636 markers: vec!["TODO:".to_string()],
637 };
638 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
639 assert_eq!(
640 todos.len(),
641 0,
642 "A TODO not at the beginning should not be detected"
643 );
644 }
645
646 #[test]
647 fn test_fixme_with_colon() {
648 let src = r#"
650 // FIXME: Correct the error handling
651 "#;
652 let config = MarkerConfig {
653 markers: vec!["FIXME".to_string()],
654 };
655 let items = test_extract_marked_items(Path::new("file.rs"), src, &config);
656 assert_eq!(items.len(), 1);
657 assert_eq!(items[0].message, "Correct the error handling");
658 }
659
660 #[test]
661 fn test_fixme_without_colon() {
662 let src = r#"
664 // FIXME Correct the error handling
665 "#;
666 let config = MarkerConfig {
667 markers: vec!["FIXME".to_string()],
668 };
669 let items = test_extract_marked_items(Path::new("file.rs"), src, &config);
670 assert_eq!(items.len(), 1);
671 assert_eq!(items[0].message, "Correct the error handling");
672 }
673
674 #[test]
675 fn test_mixed_markers() {
676 let src = r#"
679 // TODO: Implement feature A
680 // FIXME: Fix bug in module
681 // Some regular comment
682 // TODO Implement feature B
683 // FIXME Fix another bug
684 "#;
685 let config = MarkerConfig {
686 markers: vec!["TODO".to_string(), "FIXME".to_string()],
687 };
688 let items = test_extract_marked_items(Path::new("file.rs"), src, &config);
689
690 assert_eq!(items.len(), 4);
692 assert_eq!(items[0].message, "Implement feature A");
693 assert_eq!(items[1].message, "Fix bug in module");
694 assert_eq!(items[2].message, "Implement feature B");
695 assert_eq!(items[3].message, "Fix another bug");
696 }
697
698 #[test]
699 fn test_mixed_markers_complex() {
700 let src = r#"
703// TODO: Implement feature A
704
705fn some_function() {
706 // This is a normal comment
707 // FIXME: Fix bug in module
708 println!("Hello, world!");
709}
710
711/*
712 TODO: Implement feature C
713 with additional multiline details
714*/
715
716/// FIXME Fix critical bug
717/// that occurs on edge cases
718
719// TODO Implement feature B
720
721// FIXME Fix another bug
722"#;
723
724 let config = MarkerConfig {
725 markers: vec!["TODO".to_string(), "FIXME".to_string()],
726 };
727 let items = test_extract_marked_items(Path::new("file.rs"), src, &config);
728
729 assert_eq!(items.len(), 6);
737
738 assert_eq!(items[0].message, "Implement feature A");
739 assert_eq!(items[1].message, "Fix bug in module");
740 assert_eq!(
741 items[2].message,
742 "Implement feature C with additional multiline details"
743 );
744 assert_eq!(
745 items[3].message,
746 "Fix critical bug that occurs on edge cases"
747 );
748 assert_eq!(items[4].message, "Implement feature B");
749 assert_eq!(items[5].message, "Fix another bug");
750 }
751
752 #[test]
753 fn test_merge_multiline_todo_with_todo_in_str() {
754 init_logger();
755 let src = r#"
756// TODO add a new argument to specify what markers to look for
757// like --markers "TODO, FIXME, HACK"
758"#;
759 let config = MarkerConfig {
760 markers: vec!["TODO".to_string()],
761 };
762 let todos = test_extract_marked_items(Path::new("file.rs"), src, &config);
763
764 assert_eq!(todos.len(), 1);
765
766 assert_eq!(todos[0].line_number, 2);
767 assert_eq!(todos[0].message, "add a new argument to specify what markers to look for like --markers \"TODO, FIXME, HACK\"");
768 }
769
770 #[test]
771 fn test_valid_sh_extension() {
772 init_logger();
773 let src = "# TODO: setup\nexit";
774 let config = MarkerConfig {
775 markers: vec!["TODO:".to_string()],
776 };
777 let todos = test_extract_marked_items(Path::new("script.sh"), src, &config);
778 assert_eq!(todos.len(), 1);
779 assert_eq!(todos[0].marker, "TODO:");
780 }
781
782 #[test]
783 fn test_valid_yaml_extension() {
784 init_logger();
785 let src = "# TODO: conf\nkey: val";
786 let config = MarkerConfig {
787 markers: vec!["TODO:".to_string()],
788 };
789 let todos = test_extract_marked_items(Path::new("config.yaml"), src, &config);
790 assert_eq!(todos.len(), 1);
791 assert_eq!(todos[0].marker, "TODO:");
792 }
793
794 #[test]
795 fn test_valid_toml_extension() {
796 init_logger();
797 let src = "# TODO: fix\nkey=1";
798 let config = MarkerConfig {
799 markers: vec!["TODO:".to_string()],
800 };
801 let todos = test_extract_marked_items(Path::new("config.toml"), src, &config);
802 assert_eq!(todos.len(), 1);
803 assert_eq!(todos[0].marker, "TODO:");
804 }
805
806 #[test]
807 fn test_valid_sql_extension() {
808 init_logger();
809 let src = "-- TODO: q\nSELECT 1;";
810 let config = MarkerConfig {
811 markers: vec!["TODO:".to_string()],
812 };
813 let todos = test_extract_marked_items(Path::new("query.sql"), src, &config);
814 assert_eq!(todos.len(), 1);
815 assert_eq!(todos[0].marker, "TODO:");
816 }
817
818 #[test]
819 fn test_valid_markdown_extension() {
820 init_logger();
821 let src = "<!-- TODO: doc -->";
822 let config = MarkerConfig {
823 markers: vec!["TODO:".to_string()],
824 };
825 let todos = test_extract_marked_items(Path::new("README.md"), src, &config);
826 assert_eq!(todos.len(), 1);
827 assert_eq!(todos[0].marker, "TODO:");
828 }
829
830 #[test]
831 fn test_dockerfile_no_extension() {
832 init_logger();
833 let src = "# TODO: step\nFROM alpine";
834 let config = MarkerConfig {
835 markers: vec!["TODO:".to_string()],
836 };
837 let todos = test_extract_marked_items(Path::new("Dockerfile"), src, &config);
838 assert_eq!(todos.len(), 1);
839 assert_eq!(todos[0].marker, "TODO:");
840 }
841
842 #[test]
843 fn test_extract_marked_items_from_file_unsupported_extension() {
844 init_logger();
845 let config = MarkerConfig {
846 markers: vec!["TODO".to_string()],
847 };
848
849 let result = extract_marked_items_from_file(Path::new("file.unsupported"), &config);
851
852 assert!(result.is_ok());
854 assert_eq!(result.unwrap().len(), 0);
855 }
856
857 #[test]
858 fn test_extract_marked_items_from_file_nonexistent_file() {
859 init_logger();
860 let config = MarkerConfig {
861 markers: vec!["TODO".to_string()],
862 };
863
864 let result = extract_marked_items_from_file(Path::new("nonexistent_file.rs"), &config);
866
867 assert!(result.is_err());
869 let error_msg = result.unwrap_err();
870 assert!(error_msg.contains("Could not read file"));
871 assert!(error_msg.contains("nonexistent_file.rs"));
872 }
873
874 #[test]
875 fn test_extract_marked_items_from_file_permission_denied() {
876 init_logger();
877 let config = MarkerConfig {
878 markers: vec!["TODO".to_string()],
879 };
880
881 test_permission_denied_unix(&config);
882 test_permission_denied_cross_platform(&config);
883 }
884
885 #[cfg(unix)]
886 fn test_permission_denied_unix(config: &MarkerConfig) {
887 use std::fs;
888 use std::os::unix::fs::PermissionsExt;
889 use tempfile::Builder;
890
891 let temp_file = Builder::new()
893 .suffix(".rs")
894 .tempfile()
895 .expect("Failed to create temp file");
896 let temp_path = temp_file.path();
897
898 std::fs::write(temp_path, b"// TODO: test").expect("Failed to write test content");
900
901 let metadata = fs::metadata(temp_path).expect("Failed to get metadata");
903 let mut permissions = metadata.permissions();
904 permissions.set_mode(0o000); if fs::set_permissions(temp_path, permissions).is_ok() {
907 let result = extract_marked_items_from_file(temp_path, config);
908
909 assert!(result.is_err());
911 let error_msg = result.unwrap_err();
912 assert!(error_msg.contains("Could not read file"));
913
914 let mut restore_permissions = fs::metadata(temp_path).unwrap().permissions();
916 restore_permissions.set_mode(0o644);
917 let _ = fs::set_permissions(temp_path, restore_permissions);
918 }
919 }
921
922 #[cfg(not(unix))]
923 fn test_permission_denied_unix(_config: &MarkerConfig) {
924 }
926
927 fn test_permission_denied_cross_platform(config: &MarkerConfig) {
928 use std::fs;
929 use tempfile::TempDir;
930
931 let temp_dir = TempDir::new().expect("Failed to create temp directory");
933 let dir_path = temp_dir.path();
934
935 let fake_file_path = dir_path.join("test.rs");
937 fs::create_dir_all(&fake_file_path).expect("Failed to create directory");
938
939 let result = extract_marked_items_from_file(&fake_file_path, config);
940
941 assert!(result.is_err());
943 let error_msg = result.unwrap_err();
944 assert!(error_msg.contains("Could not read file"));
945
946 }
948}