• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 14434467589

13 Apr 2025 11:40PM UTC coverage: 17.889% (-13.7%) from 31.623%
14434467589

Pull #188

github

web-flow
Merge 4cb284c6c into 046ca7ecc
Pull Request #188: Rewrite facet-reflect for safety

356 of 895 new or added lines in 44 files covered. (39.78%)

710 existing lines in 19 files now uncovered.

1037 of 5797 relevant lines covered (17.89%)

7.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/facet-codegen/src/main.rs
1
use std::io::Write;
2
use std::path::Path;
3
use std::process;
4

5
mod gen_tuple_impl;
6
mod readmes;
7

8
use facet_ansi::Stylize as _;
9
use log::{error, info, warn};
10
use similar::{ChangeTag, TextDiff};
11

12
fn main() {
×
13
    facet_testhelpers::setup();
×
14

15
    let opts = Options {
16
        check: std::env::args().any(|arg| arg == "--check"),
×
17
    };
18
    let mut has_diffs = false;
×
19

20
    // Check if current directory has a Cargo.toml with [workspace]
21
    let cargo_toml_path = std::env::current_dir().unwrap().join("Cargo.toml");
×
22
    let cargo_toml_content =
×
23
        fs_err::read_to_string(cargo_toml_path).expect("Failed to read Cargo.toml");
×
24
    if !cargo_toml_content.contains("[workspace]") {
×
25
        error!("🚫 {}", "Cargo.toml does not contain [workspace] (you must run codegen from the workspace root)".red());
×
26
        panic!();
×
27
    }
28

29
    // Run all three code generation tasks in parallel
30
    let opts_clone1 = opts.clone();
×
31
    let tuple_impls_result = std::thread::spawn(move || {
×
32
        let mut local_has_diffs = false;
×
33
        generate_tuple_impls(&mut local_has_diffs, opts_clone1);
×
34
        local_has_diffs
×
35
    });
36

37
    let opts_clone2 = opts.clone();
×
38
    let readme_had_diffs = std::thread::spawn(move || readmes::generate_readme_files(opts_clone2));
×
39

40
    let opts_clone3 = opts.clone();
×
41
    let sample_code_result = std::thread::spawn(move || {
×
42
        let mut local_has_diffs = false;
×
43
        copy_cargo_expand_output(&mut local_has_diffs, &opts_clone3);
×
44
        local_has_diffs
×
45
    });
46

47
    // Collect results and update has_diffs
48
    has_diffs |= tuple_impls_result
×
49
        .join()
×
50
        .expect("tuple_impls thread panicked");
×
51
    has_diffs |= readme_had_diffs
×
52
        .join()
×
53
        .expect("readme_files thread panicked");
×
54
    has_diffs |= sample_code_result
×
55
        .join()
×
56
        .expect("sample_code thread panicked");
×
57

58
    if opts.check && has_diffs {
×
59
        // Print a big banner with error message about generated files
60
        error!("┌────────────────────────────────────────────────────────────────────────────┐");
×
61
        error!("│                                                                            │");
×
62
        error!("│  GENERATED FILES HAVE CHANGED - RUN `just codegen` TO UPDATE THEM          │");
×
63
        error!("│                                                                            │");
×
64
        error!("│  For README.md files:                                                      │");
×
65
        error!("│                                                                            │");
×
66
        error!("│  • Don't edit README.md directly - edit the README.md.in template instead  │");
×
67
        error!("│  • Then run `just codegen` to regenerate the README.md files               │");
×
68
        error!("│  • A pre-commit hook is set up by cargo-husky to do just that              │");
×
69
        error!("│                                                                            │");
×
70
        error!("│  See CONTRIBUTING.md                                                       │");
×
71
        error!("│                                                                            │");
×
72
        error!("└────────────────────────────────────────────────────────────────────────────┘");
×
73
        process::exit(1);
×
74
    }
75
}
76

77
fn copy_cargo_expand_output(has_diffs: &mut bool, opts: &Options) {
×
78
    let workspace_dir = std::env::current_dir().unwrap();
×
79
    let sample_dir = workspace_dir.join("sample");
×
80

81
    // Run cargo expand command and measure execution time
82
    let start_time = std::time::Instant::now();
×
83

84
    // Command 1: cargo rustc for expansion
85
    let cargo_expand_output = std::process::Command::new("cargo")
×
86
        .env("RUSTC_BOOTSTRAP", "1") // Necessary for -Z flags
87
        .current_dir(&sample_dir) // Set working directory instead of changing it
×
88
        .arg("rustc")
89
        .arg("--target-dir")
90
        .arg("/tmp/facet-codegen-expand") // Use a temporary, less intrusive target dir
91
        .arg("--lib") // Expand the library crate in the current directory
92
        .arg("--") // Separator for rustc flags
93
        .arg("-Zunpretty=expanded") // The flag to expand macros
94
        .output() // Execute and capture output
95
        .expect("Failed to execute cargo rustc for expansion");
96

97
    // Check if cargo rustc succeeded
98
    if !cargo_expand_output.status.success() {
×
99
        error!(
×
100
            "🚫 {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
×
101
            "cargo rustc expansion failed".red(),
×
102
            String::from_utf8_lossy(&cargo_expand_output.stderr).trim(),
×
103
            String::from_utf8_lossy(&cargo_expand_output.stdout).trim()
×
104
        );
105
        std::process::exit(1);
×
106
    }
107

108
    // Prepare the code for rustfmt: prepend the necessary lines
109
    let expanded_code = String::from_utf8(cargo_expand_output.stdout)
×
110
        .expect("Failed to convert cargo expand output to UTF-8 string");
111

112
    // Replace any ::facet:: references with crate::
113
    let expanded_code = expanded_code.replace("::facet::", "crate::");
×
114
    let expanded_code = expanded_code.replace("use facet::", "use crate::");
×
115

116
    let expanded_code = expanded_code.replace(
×
117
        "::impls::_core::marker::PhantomData",
118
        "::core::marker::PhantomData",
119
    );
120

121
    // Command 2: rustfmt to format the expanded code
122
    let mut rustfmt_cmd = std::process::Command::new("rustfmt")
×
123
        .arg("--edition")
124
        .arg("2024")
125
        .arg("--emit")
126
        .arg("stdout")
127
        .stdin(std::process::Stdio::piped()) // Prepare to pipe stdin
×
128
        .stdout(std::process::Stdio::piped()) // Capture stdout
×
129
        .stderr(std::process::Stdio::piped()) // Capture stderr
×
130
        .spawn()
131
        .expect("Failed to spawn rustfmt");
132

133
    // Write the combined code to rustfmt's stdin in a separate scope
134
    // to ensure stdin is closed, signaling EOF to rustfmt.
135
    {
136
        let mut stdin = rustfmt_cmd
×
137
            .stdin
×
138
            .take()
139
            .expect("Failed to open rustfmt stdin");
140
        stdin
×
141
            .write_all(expanded_code.as_bytes())
×
142
            .expect("Failed to write to rustfmt stdin");
143
    } // stdin is closed here
×
144

145
    // Wait for rustfmt to finish and collect its output
146
    let output = rustfmt_cmd
×
147
        .wait_with_output()
148
        .expect("Failed to wait for rustfmt");
149

150
    // Check if rustfmt succeeded (using the final 'output' variable)
151
    // Note: The original code only checked the final status, which might hide
152
    // the cargo expand error if rustfmt succeeds. We now check both stages.
153
    if !output.status.success() {
×
154
        error!(
×
155
            "🚫 {}:\n--- stderr ---\n{}\n--- stdout ---\n{}",
×
156
            "rustfmt failed".red(),
×
157
            String::from_utf8_lossy(&output.stderr).trim(),
×
158
            String::from_utf8_lossy(&output.stdout).trim()
×
159
        );
160
        // We still need to check the final status for the rest of the function
161
        // but the process might have already exited if cargo expand failed.
162
        // If rustfmt itself fails, exit here.
163
        std::process::exit(1);
×
164
    }
165
    let execution_time = start_time.elapsed();
×
166

167
    if !output.status.success() {
×
168
        error!("🚫 {}", "Cargo expand command failed".red());
×
169
        std::process::exit(1);
×
170
    }
171

172
    let expanded_code =
×
173
        String::from_utf8(output.stdout).expect("Failed to convert output to string");
×
174

175
    // First collect doc comments, then filter out lines we don't want
176
    let doc_comments = expanded_code
×
177
        .lines()
178
        .filter(|line| line.trim_start().starts_with("//!"))
×
179
        .collect::<Vec<_>>()
180
        .join("\n");
181

182
    let expanded_code = expanded_code
×
183
        .lines()
184
        .filter(|line| {
×
185
            let trimmed = line.trim_start();
×
186
            !trimmed.starts_with("#![")
×
187
                && !trimmed.starts_with("#[facet(")
×
188
                && !trimmed.starts_with("#[macro_use]")
×
189
                && !trimmed.starts_with("//!")
×
190
        })
191
        .collect::<Vec<_>>()
192
        .join("\n");
193
    let expanded_code = format!("{}\n#![allow(warnings)]\n{}", doc_comments, expanded_code);
×
194

195
    // Ensure a trailing newline for consistency
196
    let expanded_code = if expanded_code.is_empty() {
×
197
        String::new()
×
198
    } else {
199
        format!("{}\n", expanded_code)
×
200
    };
201

202
    // Write the expanded code to the target file
203
    let target_path = workspace_dir
×
204
        .join("facet")
205
        .join("src")
206
        .join("sample_generated_code.rs");
207

208
    let was_different = write_if_different(&target_path, expanded_code.into_bytes(), opts.check);
×
209
    *has_diffs |= was_different;
×
210

211
    if opts.check {
×
212
        info!(
×
213
            "✅ Checked {} (took {:?})",
×
214
            "sample_generated_code.rs".blue().green(),
×
215
            execution_time
216
        );
217
    } else if was_different {
×
218
        info!(
×
219
            "🔧 Generated {} (took {:?})",
×
220
            "sample_generated_code.rs".blue().green(),
×
221
            execution_time
222
        );
223
    } else {
224
        info!(
×
225
            "✅ No changes to {} (took {:?})",
×
226
            "sample_generated_code.rs".blue().green(),
×
227
            execution_time
228
        );
229
    }
230
}
231

232
#[derive(Debug, Clone)]
233
struct Options {
234
    check: bool,
235
}
236

237
fn check_diff(path: &Path, new_content: &[u8]) -> bool {
×
238
    if !path.exists() {
×
239
        warn!(
×
240
            "📁 {}: {}",
×
241
            path.display(),
×
242
            "would create new file".yellow()
×
243
        );
244
        return true;
×
245
    }
246

247
    let old_content = fs_err::read(path).unwrap();
×
248
    if old_content != new_content {
×
249
        let old_str = String::from_utf8_lossy(&old_content);
×
250
        let new_str = String::from_utf8_lossy(new_content);
×
251

252
        let diff = TextDiff::from_lines(&old_str, &new_str);
×
253
        info!("📝 {}", format!("Diff for {}:", path.display()).blue());
×
254

255
        // Track consecutive equal lines
256
        let mut equal_count = 0;
×
257
        let mut last_tag = None;
×
258

259
        for change in diff.iter_all_changes() {
×
260
            let tag = change.tag();
×
261

262
            // If we're switching from Equal to another tag, and we have >=4 equal lines, show the count
263
            if last_tag == Some(ChangeTag::Equal) && tag != ChangeTag::Equal && equal_count > 3 {
×
264
                info!(" {} lines omitted.", equal_count - 1);
×
265
                equal_count = 0;
×
266
            }
267

268
            match tag {
×
269
                ChangeTag::Equal => {
270
                    if equal_count == 0 {
×
271
                        // Always show the first equal line
272
                        info!(" {}", change);
×
273
                    } else if equal_count < 3 {
×
274
                        // Show the 2nd and 3rd equal lines
275
                        info!(" {}", change);
×
276
                    }
277
                    equal_count += 1;
×
278
                }
279
                ChangeTag::Delete => {
280
                    equal_count = 0;
×
281
                    info!("-{}", change.red());
×
282
                }
283
                ChangeTag::Insert => {
284
                    equal_count = 0;
×
285
                    info!("+{}", change.green());
×
286
                }
287
            }
288

289
            last_tag = Some(tag);
×
290
        }
291

292
        // Handle case where diff ends with equal lines
293
        if last_tag == Some(ChangeTag::Equal) && equal_count > 3 {
×
294
            info!(" {} lines omitted.", equal_count - 1);
×
295
        }
296

297
        return true;
×
298
    }
299
    false
×
300
}
301

302
fn write_if_different(path: &Path, content: Vec<u8>, check_mode: bool) -> bool {
×
303
    let is_different = check_diff(path, &content);
×
304
    if check_mode {
×
305
        is_different
×
306
    } else if is_different {
×
307
        info!("Overwriting {} (had changes)", path.display().blue());
×
308
        fs_err::write(path, content).expect("Failed to write file");
×
309
        true
×
310
    } else {
311
        false
×
312
    }
313
}
314

315
fn generate_tuple_impls(has_diffs: &mut bool, opts: Options) {
×
316
    // Start timer to measure execution time
317
    let start_time = std::time::Instant::now();
×
318

319
    // Define the base path and template path
NEW
320
    let base_path = Path::new("facet-core/src/impls_core/tuple.rs");
×
321

NEW
322
    let output = gen_tuple_impl::generate_tuples_impls();
×
323

324
    // Format the generated code using rustfmt
325
    let mut fmt = std::process::Command::new("rustfmt")
×
326
        .arg("--edition")
327
        .arg("2024")
328
        .stdin(std::process::Stdio::piped())
×
329
        .stdout(std::process::Stdio::piped())
×
330
        .spawn()
331
        .expect("Failed to spawn rustfmt");
332

333
    // Write to rustfmt's stdin
334
    fmt.stdin
×
335
        .take()
336
        .expect("Failed to get stdin")
337
        .write_all(output.as_bytes())
×
338
        .expect("Failed to write to rustfmt stdin");
339

340
    // Get formatted output
341
    let formatted_output = fmt.wait_with_output().expect("Failed to wait for rustfmt");
×
342
    if !formatted_output.status.success() {
×
343
        // Save the problematic output for inspection
344
        let _ = std::fs::write("/tmp/output.rs", &output);
×
345
        error!(
×
346
            "🚫 {} {}",
×
347
            "rustfmt failed to format the code.".red(),
×
348
            "The unformatted output has been saved to /tmp/output.rs for inspection.".yellow(),
×
349
        );
350

351
        error!(
×
352
            "🚫 {}",
×
353
            format!("rustfmt failed with exit code: {}", formatted_output.status).red()
×
354
        );
355
        std::process::exit(1);
×
356
    }
357

358
    let was_different = write_if_different(base_path, formatted_output.stdout, opts.check);
×
359
    *has_diffs |= was_different;
×
360

361
    // Calculate execution time
362
    let execution_time = start_time.elapsed();
×
363

364
    // Print success message with execution time
365
    if opts.check {
×
366
        info!(
×
367
            "✅ Checked {} (took {:?})",
×
368
            "tuple implementations".blue().green(),
×
369
            execution_time
370
        );
371
    } else if was_different {
×
372
        info!(
×
373
            "🔧 Generated {} (took {:?})",
×
374
            "tuple implementations".blue().green(),
×
375
            execution_time
376
        );
377
    } else {
378
        info!(
×
379
            "✅ No changes to {} (took {:?})",
×
380
            "tuple implementations".blue().green(),
×
381
            execution_time
382
        );
383
    }
384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc