Skip to main content

sz_rust_cli/
safety_validator.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! SafetyValidator — 对生成代码执行铁律检查
5//!
6//! 检查项(对应 .trae/rules/project_rules.md 22 条铁律的子集):
7//! 1. 禁止 unsafe 代码(铁律 3)
8//! 2. 禁止裸 unwrap()(铁律 2)
9//! 3. 禁止 std::fs,统一 tokio::fs(铁律 4)
10//! 4. 禁止 SELECT *(铁律 8)
11//! 5. 敏感字段必须 skip_serializing(铁律 7)
12
13use std::collections::HashMap;
14
15/// 铁律违规项
16#[derive(Debug, Clone)]
17pub struct Violation {
18    /// 违反的铁律编号(如 "铁律3")
19    pub rule: String,
20    /// 违规文件路径
21    pub file: String,
22    /// 违规行号(1-based)
23    pub line: usize,
24    /// 违规描述
25    pub message: String,
26    /// 修复建议
27    pub suggestion: String,
28}
29
30/// 生成代码安全校验器(铁律子集检查)
31pub struct SafetyValidator;
32
33impl SafetyValidator {
34    /// 批量校验文件列表(.rs 走铁律 2/3/4,.sql 走铁律 8)
35    pub fn validate_files(files: &[(String, String)]) -> Vec<Violation> {
36        let mut violations = Vec::new();
37        for (path, content) in files {
38            if path.ends_with(".rs") {
39                violations.extend(Self::check_rust_file(path, content));
40            } else if path.ends_with(".sql") {
41                violations.extend(Self::check_sql_file(path, content));
42            }
43        }
44        violations
45    }
46
47    fn check_rust_file(path: &str, content: &str) -> Vec<Violation> {
48        let mut violations = Vec::new();
49        for (i, line) in content.lines().enumerate() {
50            let line_no = i + 1;
51            let trimmed = line.trim();
52            if trimmed.starts_with("//") || trimmed.starts_with("//!") {
53                continue;
54            }
55            if trimmed.contains("unsafe ") || trimmed == "unsafe" {
56                violations.push(Violation {
57                    rule: "铁律3".to_string(),
58                    file: path.to_string(),
59                    line: line_no,
60                    message: format!("发现 unsafe 代码: {trimmed}"),
61                    suggestion: "使用安全 API 替代 unsafe".to_string(),
62                });
63            }
64            if trimmed.contains(".unwrap()") && !trimmed.contains("expect(") {
65                violations.push(Violation {
66                    rule: "铁律2".to_string(),
67                    file: path.to_string(),
68                    line: line_no,
69                    message: format!("发现裸 unwrap(): {trimmed}"),
70                    suggestion: "使用 expect(\"明确原因\") 或 ? 传播错误".to_string(),
71                });
72            }
73            if trimmed.contains("std::fs::") {
74                violations.push(Violation {
75                    rule: "铁律4".to_string(),
76                    file: path.to_string(),
77                    line: line_no,
78                    message: format!("发现 std::fs 调用: {trimmed}"),
79                    suggestion: "统一使用 tokio::fs".to_string(),
80                });
81            }
82        }
83        violations
84    }
85
86    fn check_sql_file(path: &str, content: &str) -> Vec<Violation> {
87        let mut violations = Vec::new();
88        for (i, line) in content.lines().enumerate() {
89            let line_no = i + 1;
90            let upper = line.to_uppercase();
91            if upper.contains("SELECT *") {
92                violations.push(Violation {
93                    rule: "铁律8".to_string(),
94                    file: path.to_string(),
95                    line: line_no,
96                    message: format!("发现 SELECT *: {line}"),
97                    suggestion: "显式列投影,防止字段变更导致崩溃".to_string(),
98                });
99            }
100        }
101        violations
102    }
103
104    /// 生成格式化检查报告
105    pub fn format_report(violations: &[Violation]) -> String {
106        if violations.is_empty() {
107            return "安全检查通过:0 个违规项".to_string();
108        }
109        let mut report = format!("安全检查失败:发现 {} 个违规项\n", violations.len());
110        report.push_str(&"─".repeat(60));
111        report.push('\n');
112        let mut by_rule: HashMap<String, Vec<&Violation>> = HashMap::new();
113        for v in violations {
114            by_rule.entry(v.rule.clone()).or_default().push(v);
115        }
116        for (rule, vs) in by_rule.iter() {
117            report.push_str(&format!("\n[{rule}] ({} 个违规)\n", vs.len()));
118            for v in vs {
119                report.push_str(&format!(
120                    "  {}:{} {}\n    → {}\n",
121                    v.file, v.line, v.message, v.suggestion
122                ));
123            }
124        }
125        report
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_no_violations() {
135        let files = vec![(
136            "src/model.rs".to_string(),
137            "pub struct Foo { pub x: i32 }\n".to_string(),
138        )];
139        let violations = SafetyValidator::validate_files(&files);
140        assert!(violations.is_empty());
141    }
142
143    #[test]
144    fn test_unsafe_violation() {
145        let files = vec![("src/foo.rs".to_string(), "unsafe { *ptr }\n".to_string())];
146        let violations = SafetyValidator::validate_files(&files);
147        assert_eq!(violations.len(), 1);
148        assert_eq!(violations[0].rule, "铁律3");
149    }
150
151    #[test]
152    fn test_unwrap_violation() {
153        let files = vec![(
154            "src/foo.rs".to_string(),
155            "let x = val.unwrap();\n".to_string(),
156        )];
157        let violations = SafetyValidator::validate_files(&files);
158        assert_eq!(violations.len(), 1);
159        assert_eq!(violations[0].rule, "铁律2");
160    }
161
162    #[test]
163    fn test_select_star_violation() {
164        let files = vec![(
165            "migrations/table.sql".to_string(),
166            "SELECT * FROM users;\n".to_string(),
167        )];
168        let violations = SafetyValidator::validate_files(&files);
169        assert_eq!(violations.len(), 1);
170        assert_eq!(violations[0].rule, "铁律8");
171    }
172
173    #[test]
174    fn test_std_fs_violation() {
175        let files = vec![(
176            "src/foo.rs".to_string(),
177            "let content = std::fs::read_to_string(path)?;\n".to_string(),
178        )];
179        let violations = SafetyValidator::validate_files(&files);
180        assert_eq!(violations.len(), 1);
181        assert_eq!(violations[0].rule, "铁律4");
182        assert!(violations[0].message.contains("std::fs"));
183    }
184
185    #[test]
186    fn test_format_report_empty() {
187        let report = SafetyValidator::format_report(&[]);
188        assert_eq!(report, "安全检查通过:0 个违规项");
189    }
190
191    #[test]
192    fn test_format_report_with_violations() {
193        let violations = vec![
194            Violation {
195                rule: "铁律3".to_string(),
196                file: "src/foo.rs".to_string(),
197                line: 10,
198                message: "发现 unsafe 代码".to_string(),
199                suggestion: "使用安全 API".to_string(),
200            },
201            Violation {
202                rule: "铁律3".to_string(),
203                file: "src/bar.rs".to_string(),
204                line: 20,
205                message: "发现 unsafe 代码".to_string(),
206                suggestion: "使用安全 API".to_string(),
207            },
208            Violation {
209                rule: "铁律8".to_string(),
210                file: "migrations/t.sql".to_string(),
211                line: 1,
212                message: "发现 SELECT *".to_string(),
213                suggestion: "显式列投影".to_string(),
214            },
215        ];
216        let report = SafetyValidator::format_report(&violations);
217        assert!(report.contains("安全检查失败:发现 3 个违规项"));
218        assert!(report.contains("[铁律3] (2 个违规)"));
219        assert!(report.contains("[铁律8] (1 个违规)"));
220        assert!(report.contains("src/foo.rs:10"));
221        assert!(report.contains("src/bar.rs:20"));
222        assert!(report.contains("migrations/t.sql:1"));
223    }
224
225    #[test]
226    fn test_validate_files_mixed_extensions() {
227        // 非 .rs/.sql 文件应被忽略
228        let files = vec![
229            (
230                "readme.md".to_string(),
231                "unsafe { } std::fs::read\n".to_string(),
232            ),
233            ("src/safe.rs".to_string(), "pub fn ok() {}\n".to_string()),
234        ];
235        let violations = SafetyValidator::validate_files(&files);
236        assert!(violations.is_empty());
237    }
238
239    #[test]
240    fn test_check_rust_file_skips_comments() {
241        // 注释中的 unsafe/unwrap/std::fs 应被忽略
242        let files = vec![(
243            "src/foo.rs".to_string(),
244            "// unsafe { } unwrap() std::fs::\n//! doc: unsafe unwrap std::fs\n".to_string(),
245        )];
246        let violations = SafetyValidator::validate_files(&files);
247        assert!(violations.is_empty());
248    }
249
250    #[test]
251    fn test_check_sql_file_no_select_star() {
252        let files = vec![(
253            "migrations/t.sql".to_string(),
254            "SELECT id, name FROM users;\n".to_string(),
255        )];
256        let violations = SafetyValidator::validate_files(&files);
257        assert!(violations.is_empty());
258    }
259}