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!["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(),     // Deprecated
182                    "error-chain".to_string(), // Deprecated
183                ],
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    /// Load standards from configuration
203    pub fn load() -> Result<Self> {
204        // For now, return defaults
205        // TODO: Load from configuration file or remote source
206        Ok(Self::default())
207    }
208
209    /// Save standards to configuration
210    pub fn save(&self) -> Result<()> {
211        // TODO: Save to configuration file
212        Ok(())
213    }
214
215    /// Get all clippy rules based on these standards
216    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        // Add performance and style lints
248        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    /// Generate clippy.toml configuration
261    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, // Convert to bytes approximation
295            self.documentation.require_private_docs,
296        )
297    }
298
299    /// Check if a project complies with these standards
300    pub async fn check_compliance(&self, project_path: &std::path::Path) -> Result<Vec<String>> {
301        let mut violations = Vec::new();
302
303        // Check Cargo.toml for edition
304        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        // Additional compliance checks would go here
316
317        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        // Test edition standards
333        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        // Test file limits
338        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        // Test function limits
343        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        // Test documentation standards
348        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        // Test banned patterns
354        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        // Test dependencies
363        assert!(!standards.dependencies.required.is_empty());
364        assert!(!standards.dependencies.recommended.is_empty());
365        assert!(!standards.dependencies.banned.is_empty());
366
367        // Test security standards
368        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        // Write Cargo.toml with Edition 2024
593        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        // Write Cargo.toml with wrong edition
620        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        // Should not fail if no Cargo.toml exists
653        assert!(violations.is_empty());
654    }
655
656    // Integration tests for serialization/deserialization
657    #[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    // Property-based tests
711    #[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}