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![
175 "thiserror".to_string(),
176 "anyhow".to_string(),
177 ],
178 recommended: vec![
179 "tokio".to_string(),
180 "tracing".to_string(),
181 "serde".to_string(),
182 ],
183 banned: vec![
184 "failure".to_string(), "error-chain".to_string(), ],
187 version_requirements: HashMap::new(),
188 },
189 security: SecurityStandards {
190 ban_unsafe: true,
191 require_audit: true,
192 audit_frequency_days: 30,
193 security_patterns: vec![
194 BannedPattern {
195 name: "hardcoded_secret".to_string(),
196 pattern: r#"(?i)(password|secret|key|token)\s*=\s*["'][^"']+["']"#.to_string(),
197 message: "Potential hardcoded secret detected".to_string(),
198 applies_to_tests: false,
199 },
200 ],
201 },
202 }
203 }
204}
205
206impl CodingStandards {
207 pub fn load() -> Result<Self> {
209 Ok(Self::default())
212 }
213
214 pub fn save(&self) -> Result<()> {
216 Ok(())
218 }
219
220 pub fn get_clippy_rules(&self) -> Vec<String> {
222 let mut rules = vec![
223 "-D warnings".to_string(),
224 ];
225
226 if self.banned_patterns.ban_unwrap {
227 rules.push("-D clippy::unwrap_used".to_string());
228 }
229
230 if self.banned_patterns.ban_expect {
231 rules.push("-D clippy::expect_used".to_string());
232 }
233
234 if self.banned_patterns.ban_panic {
235 rules.push("-D clippy::panic".to_string());
236 }
237
238 if self.banned_patterns.ban_todo {
239 rules.push("-D clippy::todo".to_string());
240 }
241
242 if self.banned_patterns.ban_unimplemented {
243 rules.push("-D clippy::unimplemented".to_string());
244 }
245
246 if self.documentation.require_public_docs {
247 rules.push("-D missing_docs".to_string());
248 }
249
250 if self.security.ban_unsafe {
251 rules.push("-F unsafe_code".to_string());
252 }
253
254 rules.extend([
256 "-W clippy::pedantic".to_string(),
257 "-W clippy::nursery".to_string(),
258 "-W clippy::cargo".to_string(),
259 "-D clippy::dbg_macro".to_string(),
260 "-D clippy::print_stdout".to_string(),
261 "-D clippy::print_stderr".to_string(),
262 ]);
263
264 rules
265 }
266
267 pub fn generate_clippy_config(&self) -> String {
269 format!(
270 r#"# Ferrous Forge - Rust Standards Enforcement
271# Generated automatically - do not edit manually
272
273msrv = "{}"
274max-fn-params-bools = 2
275max-struct-bools = 2
276max-trait-bounds = 2
277max-include-file-size = {}
278min-ident-chars-threshold = 2
279literal-representation-threshold = 1000
280check-private-items = {}
281missing-docs-allow-unused = false
282allow-comparison-to-zero = false
283allow-mixed-uninlined-format-args = false
284allow-one-hash-in-raw-strings = false
285allow-useless-vec-in-tests = false
286allow-indexing-slicing-in-tests = false
287allowed-idents-below-min-chars = ["i", "j", "x", "y", "z"]
288allowed-wildcard-imports = []
289allow-exact-repetitions = false
290allow-private-module-inception = false
291too-large-for-stack = 100
292upper-case-acronyms-aggressive = true
293allowed-scripts = ["Latin"]
294disallowed-names = ["foo", "bar", "baz", "qux", "quux", "test", "tmp", "temp"]
295unreadable-literal-lint-fractions = true
296semicolon-inside-block-ignore-singleline = false
297semicolon-outside-block-ignore-multiline = false
298arithmetic-side-effects-allowed = []
299"#,
300 self.edition.min_rust_version,
301 self.file_limits.max_lines * 1000, self.documentation.require_private_docs,
303 )
304 }
305
306 pub async fn check_compliance(&self, project_path: &std::path::Path) -> Result<Vec<String>> {
308 let mut violations = Vec::new();
309
310 let cargo_toml = project_path.join("Cargo.toml");
312 if cargo_toml.exists() {
313 let content = tokio::fs::read_to_string(&cargo_toml).await?;
314 if !content.contains(&format!(r#"edition = "{}""#, self.edition.required_edition)) {
315 violations.push(format!("Project must use Rust Edition {}", self.edition.required_edition));
316 }
317 }
318
319 Ok(violations)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use tempfile::TempDir;
329 use tokio::fs;
330
331 #[test]
332 fn test_coding_standards_default() {
333 let standards = CodingStandards::default();
334
335 assert_eq!(standards.edition.required_edition, "2024");
337 assert_eq!(standards.edition.min_rust_version, "1.85.0");
338 assert!(standards.edition.auto_upgrade);
339
340 assert_eq!(standards.file_limits.max_lines, 300);
342 assert_eq!(standards.file_limits.max_line_length, 100);
343 assert!(!standards.file_limits.exempt_files.is_empty());
344
345 assert_eq!(standards.function_limits.max_lines, 50);
347 assert_eq!(standards.function_limits.max_parameters, 5);
348 assert_eq!(standards.function_limits.max_complexity, 10);
349
350 assert!(standards.documentation.require_public_docs);
352 assert!(!standards.documentation.require_private_docs);
353 assert!(!standards.documentation.require_examples);
354 assert_eq!(standards.documentation.min_doc_length, 10);
355
356 assert!(standards.banned_patterns.ban_underscore_params);
358 assert!(standards.banned_patterns.ban_underscore_let);
359 assert!(standards.banned_patterns.ban_unwrap);
360 assert!(standards.banned_patterns.ban_expect);
361 assert!(standards.banned_patterns.ban_panic);
362 assert!(standards.banned_patterns.ban_todo);
363 assert!(standards.banned_patterns.ban_unimplemented);
364
365 assert!(!standards.dependencies.required.is_empty());
367 assert!(!standards.dependencies.recommended.is_empty());
368 assert!(!standards.dependencies.banned.is_empty());
369
370 assert!(standards.security.ban_unsafe);
372 assert!(standards.security.require_audit);
373 assert_eq!(standards.security.audit_frequency_days, 30);
374 assert!(!standards.security.security_patterns.is_empty());
375 }
376
377 #[test]
378 fn test_edition_standards() {
379 let standards = EditionStandards {
380 required_edition: "2021".to_string(),
381 min_rust_version: "1.70.0".to_string(),
382 auto_upgrade: false,
383 };
384
385 assert_eq!(standards.required_edition, "2021");
386 assert_eq!(standards.min_rust_version, "1.70.0");
387 assert!(!standards.auto_upgrade);
388 }
389
390 #[test]
391 fn test_file_limits() {
392 let limits = FileLimits {
393 max_lines: 500,
394 max_line_length: 120,
395 exempt_files: vec!["test.rs".to_string()],
396 };
397
398 assert_eq!(limits.max_lines, 500);
399 assert_eq!(limits.max_line_length, 120);
400 assert_eq!(limits.exempt_files.len(), 1);
401 assert_eq!(limits.exempt_files[0], "test.rs");
402 }
403
404 #[test]
405 fn test_function_limits() {
406 let limits = FunctionLimits {
407 max_lines: 100,
408 max_parameters: 8,
409 max_complexity: 15,
410 };
411
412 assert_eq!(limits.max_lines, 100);
413 assert_eq!(limits.max_parameters, 8);
414 assert_eq!(limits.max_complexity, 15);
415 }
416
417 #[test]
418 fn test_documentation_standards() {
419 let docs = DocumentationStandards {
420 require_public_docs: false,
421 require_private_docs: true,
422 require_examples: true,
423 min_doc_length: 20,
424 };
425
426 assert!(!docs.require_public_docs);
427 assert!(docs.require_private_docs);
428 assert!(docs.require_examples);
429 assert_eq!(docs.min_doc_length, 20);
430 }
431
432 #[test]
433 fn test_banned_patterns() {
434 let patterns = BannedPatterns {
435 ban_underscore_params: false,
436 ban_underscore_let: false,
437 ban_unwrap: false,
438 ban_expect: false,
439 ban_panic: false,
440 ban_todo: false,
441 ban_unimplemented: false,
442 custom_banned: vec![],
443 };
444
445 assert!(!patterns.ban_underscore_params);
446 assert!(!patterns.ban_underscore_let);
447 assert!(!patterns.ban_unwrap);
448 assert!(!patterns.ban_expect);
449 assert!(!patterns.ban_panic);
450 assert!(!patterns.ban_todo);
451 assert!(!patterns.ban_unimplemented);
452 assert!(patterns.custom_banned.is_empty());
453 }
454
455 #[test]
456 fn test_banned_pattern() {
457 let pattern = BannedPattern {
458 name: "test_pattern".to_string(),
459 pattern: r"test_.*".to_string(),
460 message: "Test pattern found".to_string(),
461 applies_to_tests: true,
462 };
463
464 assert_eq!(pattern.name, "test_pattern");
465 assert_eq!(pattern.pattern, r"test_.*");
466 assert_eq!(pattern.message, "Test pattern found");
467 assert!(pattern.applies_to_tests);
468 }
469
470 #[test]
471 fn test_dependency_standards() {
472 let mut version_reqs = HashMap::new();
473 version_reqs.insert("serde".to_string(), "1.0".to_string());
474
475 let deps = DependencyStandards {
476 required: vec!["anyhow".to_string()],
477 recommended: vec!["serde".to_string()],
478 banned: vec!["failure".to_string()],
479 version_requirements: version_reqs,
480 };
481
482 assert_eq!(deps.required.len(), 1);
483 assert_eq!(deps.recommended.len(), 1);
484 assert_eq!(deps.banned.len(), 1);
485 assert_eq!(deps.version_requirements.get("serde"), Some(&"1.0".to_string()));
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(&cargo_toml, r#"
594[package]
595name = "test"
596version = "0.1.0"
597edition = "2024"
598"#).await.expect("Failed to write Cargo.toml");
599
600 let standards = CodingStandards::default();
601 let violations = standards.check_compliance(temp_dir.path()).await.expect("Check should succeed");
602
603 assert!(violations.is_empty());
604 }
605
606 #[tokio::test]
607 async fn test_check_compliance_wrong_edition() {
608 let temp_dir = TempDir::new().expect("Failed to create temp directory");
609 let cargo_toml = temp_dir.path().join("Cargo.toml");
610
611 fs::write(&cargo_toml, r#"
613[package]
614name = "test"
615version = "0.1.0"
616edition = "2021"
617"#).await.expect("Failed to write Cargo.toml");
618
619 let standards = CodingStandards::default();
620 let violations = standards.check_compliance(temp_dir.path()).await.expect("Check should succeed");
621
622 assert!(!violations.is_empty());
623 assert!(violations[0].contains("Edition 2024"));
624 }
625
626 #[tokio::test]
627 async fn test_check_compliance_no_cargo_toml() {
628 let temp_dir = TempDir::new().expect("Failed to create temp directory");
629
630 let standards = CodingStandards::default();
631 let violations = standards.check_compliance(temp_dir.path()).await.expect("Check should succeed");
632
633 assert!(violations.is_empty());
635 }
636
637 #[test]
639 fn test_serialization_roundtrip() {
640 let original = CodingStandards::default();
641
642 let serialized = serde_json::to_string(&original).expect("Should serialize");
643 let deserialized: CodingStandards = serde_json::from_str(&serialized).expect("Should deserialize");
644
645 assert_eq!(original.edition.required_edition, deserialized.edition.required_edition);
646 assert_eq!(original.file_limits.max_lines, deserialized.file_limits.max_lines);
647 assert_eq!(original.function_limits.max_lines, deserialized.function_limits.max_lines);
648 assert_eq!(original.documentation.require_public_docs, deserialized.documentation.require_public_docs);
649 assert_eq!(original.banned_patterns.ban_unwrap, deserialized.banned_patterns.ban_unwrap);
650 assert_eq!(original.security.ban_unsafe, deserialized.security.ban_unsafe);
651 }
652
653 #[test]
654 fn test_banned_pattern_serialization() {
655 let pattern = BannedPattern {
656 name: "test".to_string(),
657 pattern: r"test_.*".to_string(),
658 message: "Test message".to_string(),
659 applies_to_tests: true,
660 };
661
662 let serialized = serde_json::to_string(&pattern).expect("Should serialize");
663 let deserialized: BannedPattern = serde_json::from_str(&serialized).expect("Should deserialize");
664
665 assert_eq!(pattern.name, deserialized.name);
666 assert_eq!(pattern.pattern, deserialized.pattern);
667 assert_eq!(pattern.message, deserialized.message);
668 assert_eq!(pattern.applies_to_tests, deserialized.applies_to_tests);
669 }
670
671 #[cfg(feature = "proptest")]
673 mod property_tests {
674 use super::*;
675 use proptest::prelude::*;
676
677 proptest! {
678 #[test]
679 fn test_file_limits_properties(
680 max_lines in 1usize..10000,
681 max_line_length in 50usize..500,
682 ) {
683 let limits = FileLimits {
684 max_lines,
685 max_line_length,
686 exempt_files: vec![],
687 };
688
689 prop_assert!(limits.max_lines > 0);
690 prop_assert!(limits.max_line_length >= 50);
691 prop_assert_eq!(limits.max_lines, max_lines);
692 prop_assert_eq!(limits.max_line_length, max_line_length);
693 }
694
695 #[test]
696 fn test_function_limits_properties(
697 max_lines in 1usize..1000,
698 max_parameters in 1usize..20,
699 max_complexity in 1usize..50,
700 ) {
701 let limits = FunctionLimits {
702 max_lines,
703 max_parameters,
704 max_complexity,
705 };
706
707 prop_assert!(limits.max_lines > 0);
708 prop_assert!(limits.max_parameters > 0);
709 prop_assert!(limits.max_complexity > 0);
710 }
711 }
712 }
713}