1use crate::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CodingStandards {
13 pub edition: EditionStandards,
15 pub file_limits: FileLimits,
17 pub function_limits: FunctionLimits,
19 pub documentation: DocumentationStandards,
21 pub banned_patterns: BannedPatterns,
23 pub dependencies: DependencyStandards,
25 pub security: SecurityStandards,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct EditionStandards {
32 pub required_edition: String,
34 pub min_rust_version: String,
36 pub auto_upgrade: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct FileLimits {
43 pub max_lines: usize,
45 pub max_line_length: usize,
47 pub exempt_files: Vec<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FunctionLimits {
54 pub max_lines: usize,
56 pub max_parameters: usize,
58 pub max_complexity: usize,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DocumentationStandards {
65 pub require_public_docs: bool,
67 pub require_private_docs: bool,
69 pub require_examples: bool,
71 pub min_doc_length: usize,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct BannedPatterns {
78 pub ban_underscore_params: bool,
80 pub ban_underscore_let: bool,
82 pub ban_unwrap: bool,
84 pub ban_expect: bool,
86 pub ban_panic: bool,
88 pub ban_todo: bool,
90 pub ban_unimplemented: bool,
92 pub custom_banned: Vec<BannedPattern>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BannedPattern {
99 pub name: String,
101 pub pattern: String,
103 pub message: String,
105 pub applies_to_tests: bool,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct DependencyStandards {
112 pub required: Vec<String>,
114 pub recommended: Vec<String>,
116 pub banned: Vec<String>,
118 pub version_requirements: HashMap<String, String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SecurityStandards {
125 pub ban_unsafe: bool,
127 pub require_audit: bool,
129 pub audit_frequency_days: u32,
131 pub security_patterns: Vec<BannedPattern>,
133}
134
135impl Default for CodingStandards {
136 fn default() -> Self {
137 Self {
138 edition: EditionStandards {
139 required_edition: "2024".to_string(),
140 min_rust_version: "1.85.0".to_string(),
141 auto_upgrade: true,
142 },
143 file_limits: FileLimits {
144 max_lines: 300,
145 max_line_length: 100,
146 exempt_files: vec![
147 "build.rs".to_string(),
148 "tests/".to_string(),
149 "benches/".to_string(),
150 ],
151 },
152 function_limits: FunctionLimits {
153 max_lines: 50,
154 max_parameters: 5,
155 max_complexity: 10,
156 },
157 documentation: DocumentationStandards {
158 require_public_docs: true,
159 require_private_docs: false,
160 require_examples: false,
161 min_doc_length: 10,
162 },
163 banned_patterns: BannedPatterns {
164 ban_underscore_params: true,
165 ban_underscore_let: true,
166 ban_unwrap: true,
167 ban_expect: true,
168 ban_panic: true,
169 ban_todo: true,
170 ban_unimplemented: true,
171 custom_banned: vec![],
172 },
173 dependencies: DependencyStandards {
174 required: vec!["thiserror".to_string(), "anyhow".to_string()],
175 recommended: vec![
176 "tokio".to_string(),
177 "tracing".to_string(),
178 "serde".to_string(),
179 ],
180 banned: vec![
181 "failure".to_string(), "error-chain".to_string(), ],
184 version_requirements: HashMap::new(),
185 },
186 security: SecurityStandards {
187 ban_unsafe: true,
188 require_audit: true,
189 audit_frequency_days: 30,
190 security_patterns: vec![BannedPattern {
191 name: "hardcoded_secret".to_string(),
192 pattern: r#"(?i)(password|secret|key|token)\s*=\s*["'][^"']+["']"#.to_string(),
193 message: "Potential hardcoded secret detected".to_string(),
194 applies_to_tests: false,
195 }],
196 },
197 }
198 }
199}
200
201impl CodingStandards {
202 pub fn load() -> Result<Self> {
204 Ok(Self::default())
207 }
208
209 pub fn save(&self) -> Result<()> {
211 Ok(())
213 }
214
215 pub fn get_clippy_rules(&self) -> Vec<String> {
217 let mut rules = vec!["-D warnings".to_string()];
218
219 if self.banned_patterns.ban_unwrap {
220 rules.push("-D clippy::unwrap_used".to_string());
221 }
222
223 if self.banned_patterns.ban_expect {
224 rules.push("-D clippy::expect_used".to_string());
225 }
226
227 if self.banned_patterns.ban_panic {
228 rules.push("-D clippy::panic".to_string());
229 }
230
231 if self.banned_patterns.ban_todo {
232 rules.push("-D clippy::todo".to_string());
233 }
234
235 if self.banned_patterns.ban_unimplemented {
236 rules.push("-D clippy::unimplemented".to_string());
237 }
238
239 if self.documentation.require_public_docs {
240 rules.push("-D missing_docs".to_string());
241 }
242
243 if self.security.ban_unsafe {
244 rules.push("-F unsafe_code".to_string());
245 }
246
247 rules.extend([
249 "-W clippy::pedantic".to_string(),
250 "-W clippy::nursery".to_string(),
251 "-W clippy::cargo".to_string(),
252 "-D clippy::dbg_macro".to_string(),
253 "-D clippy::print_stdout".to_string(),
254 "-D clippy::print_stderr".to_string(),
255 ]);
256
257 rules
258 }
259
260 pub fn generate_clippy_config(&self) -> String {
262 format!(
263 r#"# Ferrous Forge - Rust Standards Enforcement
264# Generated automatically - do not edit manually
265
266msrv = "{}"
267max-fn-params-bools = 2
268max-struct-bools = 2
269max-trait-bounds = 2
270max-include-file-size = {}
271min-ident-chars-threshold = 2
272literal-representation-threshold = 1000
273check-private-items = {}
274missing-docs-allow-unused = false
275allow-comparison-to-zero = false
276allow-mixed-uninlined-format-args = false
277allow-one-hash-in-raw-strings = false
278allow-useless-vec-in-tests = false
279allow-indexing-slicing-in-tests = false
280allowed-idents-below-min-chars = ["i", "j", "x", "y", "z"]
281allowed-wildcard-imports = []
282allow-exact-repetitions = false
283allow-private-module-inception = false
284too-large-for-stack = 100
285upper-case-acronyms-aggressive = true
286allowed-scripts = ["Latin"]
287disallowed-names = ["foo", "bar", "baz", "qux", "quux", "test", "tmp", "temp"]
288unreadable-literal-lint-fractions = true
289semicolon-inside-block-ignore-singleline = false
290semicolon-outside-block-ignore-multiline = false
291arithmetic-side-effects-allowed = []
292"#,
293 self.edition.min_rust_version,
294 self.file_limits.max_lines * 1000, self.documentation.require_private_docs,
296 )
297 }
298
299 pub async fn check_compliance(&self, project_path: &std::path::Path) -> Result<Vec<String>> {
301 let mut violations = Vec::new();
302
303 let cargo_toml = project_path.join("Cargo.toml");
305 if cargo_toml.exists() {
306 let content = tokio::fs::read_to_string(&cargo_toml).await?;
307 if !content.contains(&format!(r#"edition = "{}""#, self.edition.required_edition)) {
308 violations.push(format!(
309 "Project must use Rust Edition {}",
310 self.edition.required_edition
311 ));
312 }
313 }
314
315 Ok(violations)
318 }
319}
320
321#[cfg(test)]
322#[allow(clippy::expect_used, clippy::unwrap_used)]
323mod tests {
324 use super::*;
325 use tempfile::TempDir;
326 use tokio::fs;
327
328 #[test]
329 fn test_coding_standards_default() {
330 let standards = CodingStandards::default();
331
332 assert_eq!(standards.edition.required_edition, "2024");
334 assert_eq!(standards.edition.min_rust_version, "1.85.0");
335 assert!(standards.edition.auto_upgrade);
336
337 assert_eq!(standards.file_limits.max_lines, 300);
339 assert_eq!(standards.file_limits.max_line_length, 100);
340 assert!(!standards.file_limits.exempt_files.is_empty());
341
342 assert_eq!(standards.function_limits.max_lines, 50);
344 assert_eq!(standards.function_limits.max_parameters, 5);
345 assert_eq!(standards.function_limits.max_complexity, 10);
346
347 assert!(standards.documentation.require_public_docs);
349 assert!(!standards.documentation.require_private_docs);
350 assert!(!standards.documentation.require_examples);
351 assert_eq!(standards.documentation.min_doc_length, 10);
352
353 assert!(standards.banned_patterns.ban_underscore_params);
355 assert!(standards.banned_patterns.ban_underscore_let);
356 assert!(standards.banned_patterns.ban_unwrap);
357 assert!(standards.banned_patterns.ban_expect);
358 assert!(standards.banned_patterns.ban_panic);
359 assert!(standards.banned_patterns.ban_todo);
360 assert!(standards.banned_patterns.ban_unimplemented);
361
362 assert!(!standards.dependencies.required.is_empty());
364 assert!(!standards.dependencies.recommended.is_empty());
365 assert!(!standards.dependencies.banned.is_empty());
366
367 assert!(standards.security.ban_unsafe);
369 assert!(standards.security.require_audit);
370 assert_eq!(standards.security.audit_frequency_days, 30);
371 assert!(!standards.security.security_patterns.is_empty());
372 }
373
374 #[test]
375 fn test_edition_standards() {
376 let standards = EditionStandards {
377 required_edition: "2021".to_string(),
378 min_rust_version: "1.70.0".to_string(),
379 auto_upgrade: false,
380 };
381
382 assert_eq!(standards.required_edition, "2021");
383 assert_eq!(standards.min_rust_version, "1.70.0");
384 assert!(!standards.auto_upgrade);
385 }
386
387 #[test]
388 fn test_file_limits() {
389 let limits = FileLimits {
390 max_lines: 500,
391 max_line_length: 120,
392 exempt_files: vec!["test.rs".to_string()],
393 };
394
395 assert_eq!(limits.max_lines, 500);
396 assert_eq!(limits.max_line_length, 120);
397 assert_eq!(limits.exempt_files.len(), 1);
398 assert_eq!(limits.exempt_files[0], "test.rs");
399 }
400
401 #[test]
402 fn test_function_limits() {
403 let limits = FunctionLimits {
404 max_lines: 100,
405 max_parameters: 8,
406 max_complexity: 15,
407 };
408
409 assert_eq!(limits.max_lines, 100);
410 assert_eq!(limits.max_parameters, 8);
411 assert_eq!(limits.max_complexity, 15);
412 }
413
414 #[test]
415 fn test_documentation_standards() {
416 let docs = DocumentationStandards {
417 require_public_docs: false,
418 require_private_docs: true,
419 require_examples: true,
420 min_doc_length: 20,
421 };
422
423 assert!(!docs.require_public_docs);
424 assert!(docs.require_private_docs);
425 assert!(docs.require_examples);
426 assert_eq!(docs.min_doc_length, 20);
427 }
428
429 #[test]
430 fn test_banned_patterns() {
431 let patterns = BannedPatterns {
432 ban_underscore_params: false,
433 ban_underscore_let: false,
434 ban_unwrap: false,
435 ban_expect: false,
436 ban_panic: false,
437 ban_todo: false,
438 ban_unimplemented: false,
439 custom_banned: vec![],
440 };
441
442 assert!(!patterns.ban_underscore_params);
443 assert!(!patterns.ban_underscore_let);
444 assert!(!patterns.ban_unwrap);
445 assert!(!patterns.ban_expect);
446 assert!(!patterns.ban_panic);
447 assert!(!patterns.ban_todo);
448 assert!(!patterns.ban_unimplemented);
449 assert!(patterns.custom_banned.is_empty());
450 }
451
452 #[test]
453 fn test_banned_pattern() {
454 let pattern = BannedPattern {
455 name: "test_pattern".to_string(),
456 pattern: r"test_.*".to_string(),
457 message: "Test pattern found".to_string(),
458 applies_to_tests: true,
459 };
460
461 assert_eq!(pattern.name, "test_pattern");
462 assert_eq!(pattern.pattern, r"test_.*");
463 assert_eq!(pattern.message, "Test pattern found");
464 assert!(pattern.applies_to_tests);
465 }
466
467 #[test]
468 fn test_dependency_standards() {
469 let mut version_reqs = HashMap::new();
470 version_reqs.insert("serde".to_string(), "1.0".to_string());
471
472 let deps = DependencyStandards {
473 required: vec!["anyhow".to_string()],
474 recommended: vec!["serde".to_string()],
475 banned: vec!["failure".to_string()],
476 version_requirements: version_reqs,
477 };
478
479 assert_eq!(deps.required.len(), 1);
480 assert_eq!(deps.recommended.len(), 1);
481 assert_eq!(deps.banned.len(), 1);
482 assert_eq!(
483 deps.version_requirements.get("serde"),
484 Some(&"1.0".to_string())
485 );
486 }
487
488 #[test]
489 fn test_security_standards() {
490 let security = SecurityStandards {
491 ban_unsafe: false,
492 require_audit: false,
493 audit_frequency_days: 60,
494 security_patterns: vec![],
495 };
496
497 assert!(!security.ban_unsafe);
498 assert!(!security.require_audit);
499 assert_eq!(security.audit_frequency_days, 60);
500 assert!(security.security_patterns.is_empty());
501 }
502
503 #[test]
504 fn test_load_standards() {
505 let result = CodingStandards::load();
506 assert!(result.is_ok());
507
508 let standards = result.expect("Should load standards");
509 assert_eq!(standards.edition.required_edition, "2024");
510 }
511
512 #[test]
513 fn test_save_standards() {
514 let standards = CodingStandards::default();
515 let result = standards.save();
516 assert!(result.is_ok());
517 }
518
519 #[test]
520 fn test_get_clippy_rules_all_enabled() {
521 let standards = CodingStandards::default();
522 let rules = standards.get_clippy_rules();
523
524 assert!(!rules.is_empty());
525 assert!(rules.contains(&"-D warnings".to_string()));
526 assert!(rules.contains(&"-D clippy::unwrap_used".to_string()));
527 assert!(rules.contains(&"-D clippy::expect_used".to_string()));
528 assert!(rules.contains(&"-D clippy::panic".to_string()));
529 assert!(rules.contains(&"-D clippy::todo".to_string()));
530 assert!(rules.contains(&"-D clippy::unimplemented".to_string()));
531 assert!(rules.contains(&"-D missing_docs".to_string()));
532 assert!(rules.contains(&"-F unsafe_code".to_string()));
533 assert!(rules.contains(&"-W clippy::pedantic".to_string()));
534 assert!(rules.contains(&"-W clippy::nursery".to_string()));
535 assert!(rules.contains(&"-W clippy::cargo".to_string()));
536 }
537
538 #[test]
539 fn test_get_clippy_rules_disabled() {
540 let mut standards = CodingStandards::default();
541 standards.banned_patterns.ban_unwrap = false;
542 standards.banned_patterns.ban_expect = false;
543 standards.banned_patterns.ban_panic = false;
544 standards.banned_patterns.ban_todo = false;
545 standards.banned_patterns.ban_unimplemented = false;
546 standards.documentation.require_public_docs = false;
547 standards.security.ban_unsafe = false;
548
549 let rules = standards.get_clippy_rules();
550
551 assert!(rules.contains(&"-D warnings".to_string()));
552 assert!(!rules.contains(&"-D clippy::unwrap_used".to_string()));
553 assert!(!rules.contains(&"-D clippy::expect_used".to_string()));
554 assert!(!rules.contains(&"-D clippy::panic".to_string()));
555 assert!(!rules.contains(&"-D clippy::todo".to_string()));
556 assert!(!rules.contains(&"-D clippy::unimplemented".to_string()));
557 assert!(!rules.contains(&"-D missing_docs".to_string()));
558 assert!(!rules.contains(&"-F unsafe_code".to_string()));
559 }
560
561 #[test]
562 fn test_generate_clippy_config() {
563 let standards = CodingStandards::default();
564 let config = standards.generate_clippy_config();
565
566 assert!(config.contains("Ferrous Forge"));
567 assert!(config.contains(&standards.edition.min_rust_version));
568 assert!(config.contains("max-fn-params-bools"));
569 assert!(config.contains("check-private-items"));
570 assert!(config.contains("disallowed-names"));
571 assert!(config.contains("allowed-idents-below-min-chars"));
572 }
573
574 #[test]
575 fn test_generate_clippy_config_private_docs() {
576 let mut standards = CodingStandards::default();
577 standards.documentation.require_private_docs = true;
578
579 let config = standards.generate_clippy_config();
580 assert!(config.contains("check-private-items = true"));
581
582 standards.documentation.require_private_docs = false;
583 let config = standards.generate_clippy_config();
584 assert!(config.contains("check-private-items = false"));
585 }
586
587 #[tokio::test]
588 async fn test_check_compliance_edition_2024() {
589 let temp_dir = TempDir::new().expect("Failed to create temp directory");
590 let cargo_toml = temp_dir.path().join("Cargo.toml");
591
592 fs::write(
594 &cargo_toml,
595 r#"
596[package]
597name = "test"
598version = "0.1.0"
599edition = "2024"
600"#,
601 )
602 .await
603 .expect("Failed to write Cargo.toml");
604
605 let standards = CodingStandards::default();
606 let violations = standards
607 .check_compliance(temp_dir.path())
608 .await
609 .expect("Check should succeed");
610
611 assert!(violations.is_empty());
612 }
613
614 #[tokio::test]
615 async fn test_check_compliance_wrong_edition() {
616 let temp_dir = TempDir::new().expect("Failed to create temp directory");
617 let cargo_toml = temp_dir.path().join("Cargo.toml");
618
619 fs::write(
621 &cargo_toml,
622 r#"
623[package]
624name = "test"
625version = "0.1.0"
626edition = "2021"
627"#,
628 )
629 .await
630 .expect("Failed to write Cargo.toml");
631
632 let standards = CodingStandards::default();
633 let violations = standards
634 .check_compliance(temp_dir.path())
635 .await
636 .expect("Check should succeed");
637
638 assert!(!violations.is_empty());
639 assert!(violations[0].contains("Edition 2024"));
640 }
641
642 #[tokio::test]
643 async fn test_check_compliance_no_cargo_toml() {
644 let temp_dir = TempDir::new().expect("Failed to create temp directory");
645
646 let standards = CodingStandards::default();
647 let violations = standards
648 .check_compliance(temp_dir.path())
649 .await
650 .expect("Check should succeed");
651
652 assert!(violations.is_empty());
654 }
655
656 #[test]
658 fn test_serialization_roundtrip() {
659 let original = CodingStandards::default();
660
661 let serialized = serde_json::to_string(&original).expect("Should serialize");
662 let deserialized: CodingStandards =
663 serde_json::from_str(&serialized).expect("Should deserialize");
664
665 assert_eq!(
666 original.edition.required_edition,
667 deserialized.edition.required_edition
668 );
669 assert_eq!(
670 original.file_limits.max_lines,
671 deserialized.file_limits.max_lines
672 );
673 assert_eq!(
674 original.function_limits.max_lines,
675 deserialized.function_limits.max_lines
676 );
677 assert_eq!(
678 original.documentation.require_public_docs,
679 deserialized.documentation.require_public_docs
680 );
681 assert_eq!(
682 original.banned_patterns.ban_unwrap,
683 deserialized.banned_patterns.ban_unwrap
684 );
685 assert_eq!(
686 original.security.ban_unsafe,
687 deserialized.security.ban_unsafe
688 );
689 }
690
691 #[test]
692 fn test_banned_pattern_serialization() {
693 let pattern = BannedPattern {
694 name: "test".to_string(),
695 pattern: r"test_.*".to_string(),
696 message: "Test message".to_string(),
697 applies_to_tests: true,
698 };
699
700 let serialized = serde_json::to_string(&pattern).expect("Should serialize");
701 let deserialized: BannedPattern =
702 serde_json::from_str(&serialized).expect("Should deserialize");
703
704 assert_eq!(pattern.name, deserialized.name);
705 assert_eq!(pattern.pattern, deserialized.pattern);
706 assert_eq!(pattern.message, deserialized.message);
707 assert_eq!(pattern.applies_to_tests, deserialized.applies_to_tests);
708 }
709
710 #[cfg(test)]
712 mod property_tests {
713 use super::*;
714 use proptest::prelude::*;
715
716 proptest! {
717 #[test]
718 fn test_file_limits_properties(
719 max_lines in 1usize..10000,
720 max_line_length in 50usize..500,
721 ) {
722 let limits = FileLimits {
723 max_lines,
724 max_line_length,
725 exempt_files: vec![],
726 };
727
728 prop_assert!(limits.max_lines > 0);
729 prop_assert!(limits.max_line_length >= 50);
730 prop_assert_eq!(limits.max_lines, max_lines);
731 prop_assert_eq!(limits.max_line_length, max_line_length);
732 }
733
734 #[test]
735 fn test_function_limits_properties(
736 max_lines in 1usize..1000,
737 max_parameters in 1usize..20,
738 max_complexity in 1usize..50,
739 ) {
740 let limits = FunctionLimits {
741 max_lines,
742 max_parameters,
743 max_complexity,
744 };
745
746 prop_assert!(limits.max_lines > 0);
747 prop_assert!(limits.max_parameters > 0);
748 prop_assert!(limits.max_complexity > 0);
749 }
750 }
751 }
752}