ferrous_forge/
standards.rs

1//! Rust coding standards definitions and enforcement
2//!
3//! This module defines the specific standards that Ferrous Forge enforces
4//! and provides utilities for checking compliance.
5
6use crate::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Rust coding standards enforced by Ferrous Forge
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CodingStandards {
13    /// Rust edition requirements
14    pub edition: EditionStandards,
15    /// File size limits
16    pub file_limits: FileLimits,
17    /// Function size limits
18    pub function_limits: FunctionLimits,
19    /// Documentation requirements
20    pub documentation: DocumentationStandards,
21    /// Banned patterns and practices
22    pub banned_patterns: BannedPatterns,
23    /// Dependency requirements
24    pub dependencies: DependencyStandards,
25    /// Security requirements
26    pub security: SecurityStandards,
27}
28
29/// Rust edition requirements
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct EditionStandards {
32    /// Required Rust edition
33    pub required_edition: String,
34    /// Minimum Rust version
35    pub min_rust_version: String,
36    /// Whether to automatically upgrade projects
37    pub auto_upgrade: bool,
38}
39
40/// File size limits
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct FileLimits {
43    /// Maximum lines per file
44    pub max_lines: usize,
45    /// Maximum characters per line
46    pub max_line_length: usize,
47    /// Files that are exempt from size limits
48    pub exempt_files: Vec<String>,
49}
50
51/// Function size limits
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FunctionLimits {
54    /// Maximum lines per function
55    pub max_lines: usize,
56    /// Maximum parameters per function
57    pub max_parameters: usize,
58    /// Maximum cyclomatic complexity
59    pub max_complexity: usize,
60}
61
62/// Documentation requirements
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DocumentationStandards {
65    /// Require documentation for all public items
66    pub require_public_docs: bool,
67    /// Require documentation for private items
68    pub require_private_docs: bool,
69    /// Require examples in documentation
70    pub require_examples: bool,
71    /// Minimum documentation length (words)
72    pub min_doc_length: usize,
73}
74
75/// Banned patterns and practices
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct BannedPatterns {
78    /// Ban underscore parameter naming (_param)
79    pub ban_underscore_params: bool,
80    /// Ban underscore let assignments (let _ =)
81    pub ban_underscore_let: bool,
82    /// Ban .unwrap() calls in production
83    pub ban_unwrap: bool,
84    /// Ban .expect() calls in production
85    pub ban_expect: bool,
86    /// Ban panic! macro in production
87    pub ban_panic: bool,
88    /// Ban todo! macro in production
89    pub ban_todo: bool,
90    /// Ban unimplemented! macro in production
91    pub ban_unimplemented: bool,
92    /// Custom banned patterns (regex)
93    pub custom_banned: Vec<BannedPattern>,
94}
95
96/// A custom banned pattern
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BannedPattern {
99    /// Name of the pattern
100    pub name: String,
101    /// Regular expression to match
102    pub pattern: String,
103    /// Error message to show
104    pub message: String,
105    /// Whether this applies to test code
106    pub applies_to_tests: bool,
107}
108
109/// Dependency requirements
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct DependencyStandards {
112    /// Required dependencies for all projects
113    pub required: Vec<String>,
114    /// Recommended dependencies
115    pub recommended: Vec<String>,
116    /// Banned dependencies
117    pub banned: Vec<String>,
118    /// Version requirements
119    pub version_requirements: HashMap<String, String>,
120}
121
122/// Security requirements
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct SecurityStandards {
125    /// Ban unsafe code blocks
126    pub ban_unsafe: bool,
127    /// Require security audit dependencies
128    pub require_audit: bool,
129    /// Minimum security audit frequency (days)
130    pub audit_frequency_days: u32,
131    /// Security-sensitive patterns to flag
132    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(), // Deprecated
185                    "error-chain".to_string(), // Deprecated
186                ],
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    /// Load standards from configuration
208    pub fn load() -> Result<Self> {
209        // For now, return defaults
210        // TODO: Load from configuration file or remote source
211        Ok(Self::default())
212    }
213
214    /// Save standards to configuration
215    pub fn save(&self) -> Result<()> {
216        // TODO: Save to configuration file
217        Ok(())
218    }
219
220    /// Get all clippy rules based on these standards
221    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        // Add performance and style lints
255        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    /// Generate clippy.toml configuration
268    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, // Convert to bytes approximation
302            self.documentation.require_private_docs,
303        )
304    }
305
306    /// Check if a project complies with these standards
307    pub async fn check_compliance(&self, project_path: &std::path::Path) -> Result<Vec<String>> {
308        let mut violations = Vec::new();
309        
310        // Check Cargo.toml for edition
311        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        // Additional compliance checks would go here
320        
321        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        // Test edition standards
336        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        // Test file limits
341        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        // Test function limits
346        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        // Test documentation standards
351        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        // Test banned patterns
357        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        // Test dependencies
366        assert!(!standards.dependencies.required.is_empty());
367        assert!(!standards.dependencies.recommended.is_empty());
368        assert!(!standards.dependencies.banned.is_empty());
369        
370        // Test security standards
371        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        // Write Cargo.toml with Edition 2024
593        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        // Write Cargo.toml with wrong edition
612        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        // Should not fail if no Cargo.toml exists
634        assert!(violations.is_empty());
635    }
636
637    // Integration tests for serialization/deserialization
638    #[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    // Property-based tests
672    #[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}