1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#![cfg_attr(coverage_nightly, coverage(off))]
//! CB-800 Series: Scala Best Practices Detection
//!
//! Pattern-based Scala defect detection for `pmat comply check`.
//! Based on: Odersky et al. (2004), Scalastyle, WartRemover,
//! Scala style guide conventions.
use super::types::*;
use std::fs;
use std::path::{Path, PathBuf};
/// Directories to skip when walking for Scala files.
const SKIP_DIRS: &[&str] = &[
".git",
".claude",
"node_modules",
"target",
".pmat",
"vendor",
"build",
"dist",
".bsp",
".metals",
".bloop",
".idea",
"project/target",
];
// =============================================================================
// File walking
// =============================================================================
/// Walk directory recursively for `.scala` and `.sc` files.
pub fn walkdir_scala_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
walk_scala_recursive(dir, &mut files);
files
}
fn walk_scala_recursive(dir: &Path, files: &mut Vec<PathBuf>) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !SKIP_DIRS.contains(&dir_name) {
walk_scala_recursive(&path, files);
}
} else if path
.extension()
.and_then(|e| e.to_str())
.map(|e| matches!(e, "scala" | "sc"))
.unwrap_or(false)
{
files.push(path);
}
}
}
/// Check if a Scala file is a test file.
pub fn is_scala_test_file(path: &Path) -> bool {
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if stem.ends_with("Test")
|| stem.ends_with("Spec")
|| stem.ends_with("Suite")
|| stem.starts_with("Test")
{
return true;
}
path.components().any(|c| {
let s = c.as_os_str().to_str().unwrap_or("");
s == "test" || s == "tests" || s == "it" || s == "spec"
})
}
/// Compute production lines (strip Scala comments).
pub fn compute_scala_production_lines(content: &str) -> Vec<(usize, String)> {
let mut result = Vec::new();
let mut in_block_comment = false;
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim();
if in_block_comment {
if trimmed.contains("*/") {
in_block_comment = false;
}
continue;
}
if trimmed.starts_with("/*") {
if !trimmed.contains("*/") {
in_block_comment = true;
}
continue;
}
if trimmed.starts_with("//") || trimmed.is_empty() {
continue;
}
// Strip inline comments
let line_content = if let Some(pos) = trimmed.find("//") {
// Avoid stripping URLs (http://) and string literals
if pos > 0 && &trimmed[pos - 1..pos] == ":" {
trimmed
} else {
trimmed[..pos].trim()
}
} else {
trimmed
};
if !line_content.is_empty() {
result.push((i + 1, line_content.to_string()));
}
}
result
}
// =============================================================================
// CB-800 through CB-802: Mutable collections, null usage, wildcard imports
// =============================================================================
include!("scala_bp_mutable_null_wildcard.rs");
// =============================================================================
// CB-803 through CB-805: Return statements, var declarations, blocking futures
// =============================================================================
include!("scala_bp_return_var_future.rs");