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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/// Extracts patterns from AST
pub struct PatternExtractor {
config: EntropyConfig,
}
impl PatternExtractor {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
/// Create a new instance.
pub fn new(config: EntropyConfig) -> Self {
Self { config }
}
/// Extract patterns from every source file under `project_path`.
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn extract_patterns(&self, project_path: &Path) -> Result<PatternCollection> {
if !project_path.exists() {
anyhow::bail!(
"entropy analysis path does not exist: {}",
project_path.display()
);
}
// Files arrive in path order (ProjectContext is a BTreeMap), so the walk
// and everything derived from it is reproducible.
let context = self.get_project_context(project_path).await?;
let mut collection = PatternCollection::new();
for (file_path, ast_data) in &context.files {
if self.should_process_file(file_path) {
self.extract_file_patterns(file_path, ast_data, &mut collection)?;
collection.total_files += 1;
// Measured, not estimated: the number of source lines we actually
// read. Reporting 0 here for a populated crate (defect #650) made
// the metrics block indistinguishable from an empty directory.
collection.total_loc += Self::count_source_lines(ast_data);
}
}
// Post-process to calculate variations
self.calculate_pattern_variations(&mut collection);
Ok(collection)
}
/// Count non-blank source lines in a file's contents.
fn count_source_lines(content: &str) -> usize {
content.lines().filter(|l| !l.trim().is_empty()).count()
}
/// Read the source files to analyze.
///
/// DETERMINISM: this used to shell out to `pmat context <path> --format json`
/// and only scan the directory when that call failed. That made the analyzed
/// file set depend on whichever `pmat` happened to be on `$PATH` (a stale or
/// differently-configured binary yields a different file set, and a context
/// payload without `files[].content` silently yielded zero files). The
/// directory scan is now the only path: same directory in, same file set out.
async fn get_project_context(&self, project_path: &Path) -> Result<ProjectContext> {
self.scan_source_files(project_path).await
}
/// Walk `project_path` and read every source file we know how to analyze.
async fn scan_source_files(&self, project_path: &Path) -> Result<ProjectContext> {
use std::fs;
use walkdir::WalkDir;
// BTreeMap: WalkDir yields entries in readdir order, which is not stable
// across machines or runs; the map re-imposes path order.
let mut files = BTreeMap::new();
// Walk directory and read Rust files
for entry in WalkDir::new(project_path)
.follow_links(false)
.into_iter()
.filter_map(std::result::Result::ok)
{
let path = entry.path();
// Process Rust and Ruchy files
if let Some(extension) = path.extension() {
if (extension == "rs" || extension == "ruchy" || extension == "rh")
&& self.should_process_file(path)
{
match fs::read_to_string(path) {
Ok(content) => {
files.insert(path.to_path_buf(), content);
}
Err(_) => continue, // Skip files we can't read
}
}
}
}
Ok(ProjectContext { files })
}
/// Check if file should be processed
fn should_process_file(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
!self.config.exclude_paths.iter().any(|pattern| {
glob::Pattern::new(pattern)
.map(|p| p.matches(&path_str))
.unwrap_or(false)
})
}
/// Extract patterns from a single file's AST
fn extract_file_patterns(
&self,
file_path: &Path,
ast_data: &str,
collection: &mut PatternCollection,
) -> Result<()> {
// Extract patterns using regex-based AST pattern matching
// Language-specific extraction based on file extension
if let Some(extension) = file_path.extension().and_then(|s| s.to_str()) {
match extension {
"ruchy" | "rh" => {
// Ruchy-specific pattern extraction
self.extract_ruchy_actor_patterns(file_path, ast_data, collection)?;
self.extract_ruchy_pipeline_patterns(file_path, ast_data, collection)?;
self.extract_ruchy_message_passing_patterns(file_path, ast_data, collection)?;
self.extract_ruchy_error_handling_patterns(file_path, ast_data, collection)?;
self.extract_ruchy_pattern_matching_patterns(file_path, ast_data, collection)?;
}
"rs" => {
// Standard Rust pattern extraction
self.extract_error_handling_patterns(file_path, ast_data, collection)?;
self.extract_data_validation_patterns(file_path, ast_data, collection)?;
self.extract_resource_management_patterns(file_path, ast_data, collection)?;
self.extract_control_flow_patterns(file_path, ast_data, collection)?;
self.extract_data_transformation_patterns(file_path, ast_data, collection)?;
self.extract_api_call_patterns(file_path, ast_data, collection)?;
}
_ => {
// Generic pattern extraction for other languages
self.extract_control_flow_patterns(file_path, ast_data, collection)?;
self.extract_data_transformation_patterns(file_path, ast_data, collection)?;
}
}
}
Ok(())
}
}