thag_rs 0.2.0

A versatile cross-platform playground and REPL for Rust snippets, expressions and programs. Accepts a script file or dynamic options.
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
#![allow(clippy::uninlined_format_args)]
use crate::{
    ast::{infer_deps_from_ast, infer_deps_from_source},
    code_utils::get_source_path,
    config::DependencyInference,
    maybe_config, Ast, BuildState, Dependencies, Style, ThagError, ThagResult,
};
use cargo_lookup::{Package, Query, Release};
use cargo_toml::{Dependency, DependencyDetail, Edition, Manifest};
use regex::Regex;
use serde_merge::omerge;
use std::{collections::BTreeMap, env, path::PathBuf, str::FromStr, time::Instant};
use syn::{parse_file, File};
use thag_common::{debug_log, get_verbosity, re, vprtln, V};
use thag_proc_macros::styled;
use thag_profiler::{end, profile, profiled};
use thag_styling::{svprtln, AnsiStyleExt, Role};

#[cfg(debug_assertions)]
use crate::debug_timings;

#[allow(clippy::missing_panics_doc)]
#[must_use]
/// Looks up a crate's latest stable version using cargo-lookup.
///
/// Attempts to find the crate by name, trying both the original name and a hyphenated
/// version (replacing underscores with hyphens). Returns the crate name and version
/// if found.
///
/// # Arguments
/// * `dep_crate` - The name of the crate to look up
///
/// # Returns
/// * `Some((name, version))` if the crate is found with a stable version
/// * `None` if the crate is not found or has no stable versions
#[profiled]
pub fn cargo_lookup(dep_crate: &str) -> Option<(String, String)> {
    // Try both original and hyphenated versions
    let crate_variants = vec![dep_crate.to_string(), dep_crate.replace('_', "-")];

    for crate_name in crate_variants {
        let query: Query = match crate_name.parse() {
            Ok(q) => q,
            Err(e) => {
                debug_log!("Failed to parse query for crate {}: {}", crate_name, e);
                continue;
            }
        };

        match query.package() {
            Ok(package) => {
                debug_log!(
                    "Found package {} with {} releases",
                    package.name(),
                    package.releases().len()
                );

                // Log all available versions and their pre-release status
                // #[cfg(debug_assertions)]
                // for release in package.releases() {
                //     debug_log!(
                //         "Version {} {}",
                //         release.vers,
                //         if release.vers.pre.is_empty() {
                //             "(stable)"
                //         } else {
                //             "(pre-release)"
                //         }
                //     );
                // }

                let release = highest_release(&package);

                match release {
                    Some(r) => {
                        debug_log!("Selected stable version: {}", r.vers);
                        let name = r.name.clone();
                        let version = r.vers.to_string();

                        // Check if either variant matches
                        if name == dep_crate || name == dep_crate.replace('_', "-") {
                            return Some((name, version));
                        }
                    }
                    None => {
                        debug_log!("No stable version found for {}", crate_name);
                    }
                }
            }
            Err(e) => {
                debug_log!("Failed to look up crate {}: {}", crate_name, e);
            }
        }
    }

    None
}

/// Returns the highest non-yanked release for a package, matching how
/// `cargo search` resolves versions.
///
/// Note:
/// - `cargo_lookup::Package::latest()` returns the *last uploaded* release,
///   which may not be the highest semver (e.g. an 0.8.x patch uploaded after
///   a 0.9.x release).
/// - To mirror `cargo search`, we must manually:
///   1. Filter out yanked releases,
///   2. Compare using semver ordering,
///   3. Return the highest version.
///
/// Example:
/// If crates.io has `0.9.1`, `0.9.0` (yanked), and `0.8.1` (uploaded last),
/// `Package::latest()` gives `0.8.1`, but this function returns `0.9.1`.
fn highest_release(pkg: &Package) -> Option<&Release> {
    pkg.releases()
        .iter()
        .filter(|r| !r.yanked)
        .filter(|r| r.vers.pre.is_empty())
        .max_by_key(|r| r.vers.clone()) // vers is already semver::Version
}

/// Attempt to capture the dependency name and version from the first line returned by
/// Cargo from the search by dependency name.
/// # Errors
/// Will return `Err` if the first line does not match the expected crate name and a valid version number.
/// # Panics
/// Will panic if the regular expression is malformed.
#[profiled]
pub fn capture_dep(first_line: &str) -> ThagResult<(String, String)> {
    debug_log!("first_line={first_line}");
    let re: &Regex = re!(r#"^(?P<name>[\w-]+) = "(?P<version>\d+\.\d+\.\d+)"#);

    let (name, version) = if re.is_match(first_line) {
        let captures = re.captures(first_line).unwrap();
        let name = captures.get(1).unwrap().as_str();
        let version = captures.get(2).unwrap().as_str();
        // vprtln!(V::N, "Dependency name: {}", name);
        // vprtln!(V::N, "Dependency version: {}", version);
        (String::from(name), String::from(version))
    } else {
        vprtln!(V::QQ, "Not a valid Cargo dependency format.");
        return Err("Not a valid Cargo dependency format".into());
    };
    Ok((name, version))
}

/// Configure the default manifest from the `BuildState` instance.
/// # Errors
/// Will return `Err` if there is any error parsing the default manifest.
#[profiled]
pub fn configure_default(build_state: &BuildState) -> ThagResult<Manifest> {
    let source_stem = &build_state.source_stem;

    let gen_src_path = get_source_path(build_state);

    debug_log!(
        r"build_state.build_from_orig_source={}
gen_src_path={gen_src_path}",
        build_state.build_from_orig_source
    );

    default(source_stem, &gen_src_path)
}

/// Parse the default manifest from a string template.
/// # Errors
/// Will return `Err` if there is any error parsing the default manifest.
#[profiled]
pub fn default(source_stem: &str, gen_src_path: &str) -> ThagResult<Manifest> {
    let cargo_manifest = format!(
        r#"[package]
name = "{}"
version = "0.0.1"
edition = "2021"

[dependencies]

[features]

[patch]

[workspace]

[[bin]]
name = "{}"
path = "{}"
"#,
        source_stem, source_stem, gen_src_path
    );

    // vprtln!(V::N, "cargo_manifest=\n{cargo_manifest}");

    Ok(Manifest::from_str(&cargo_manifest)?)
}

/// Merge manifest data harvested from the source script and its optional embedded toml block
/// into the default manifest.
/// # Errors
/// Will return `Err` if there is any error parsing the default manifest.
#[profiled]
pub fn merge(build_state: &mut BuildState, rs_source: &str) -> ThagResult<()> {
    #[cfg(debug_assertions)]
    let start_merge_manifest = Instant::now();

    // Take ownership of the default manifest
    let default_cargo_manifest = configure_default(build_state)?;
    let cargo_manifest = build_state
        .cargo_manifest
        .take()
        .map_or(default_cargo_manifest, |manifest| manifest);

    // let rs_inferred_deps = syntax_tree
    //     .as_ref()
    //     .map_or_else(|| infer_deps_from_source(rs_source), infer_deps_from_ast);

    profile!(infer_deps, time);
    let rs_inferred_deps = if let Some(ref use_crates) = build_state.crates_finder {
        build_state.metadata_finder.as_ref().map_or_else(
            || infer_deps_from_source(rs_source),
            |metadata_finder| infer_deps_from_ast(use_crates, metadata_finder),
        )
    } else {
        infer_deps_from_source(rs_source)
    };
    end!(infer_deps);

    // debug_log!("build_state.rs_manifest={0:#?}\n", build_state.rs_manifest);

    profile!(merge_manifest, time);
    let merged_manifest = if let Some(ref mut rs_manifest) = build_state.rs_manifest {
        if !rs_inferred_deps.is_empty() {
            #[cfg(debug_assertions)]
            debug_log!(
                "rs_dep_map (before inferred) {:#?}",
                rs_manifest.dependencies
            );
            lookup_deps(
                &build_state.infer,
                &rs_inferred_deps,
                &mut rs_manifest.dependencies,
            );

            #[cfg(debug_assertions)]
            debug_log!(
                "rs_dep_map (after inferred) {:#?}",
                rs_manifest.dependencies
            );
        }

        call_omerge(&cargo_manifest, rs_manifest)?
    } else {
        cargo_manifest
    };

    // Reassign the merged manifest back to build_state
    build_state.cargo_manifest = Some(merged_manifest);
    end!(merge_manifest);

    #[cfg(debug_assertions)]
    debug_timings(&start_merge_manifest, "Processed features");
    Ok(())
}

#[profiled]
fn call_omerge(cargo_manifest: &Manifest, rs_manifest: &mut Manifest) -> ThagResult<Manifest> {
    // eprintln!("cargo_manifest={cargo_manifest:#?}, rs_manifest={rs_manifest:#?}");
    Ok(omerge(cargo_manifest, rs_manifest)?)
}

/// Identify use ... as statements for inclusion in / exclusion from Cargo.toml metadata.
///
/// Include the "from" name and exclude the "to" name.
/// Fallback version for when an abstract syntax tree cannot be parsed.
#[must_use]
#[profiled]
pub fn find_use_renames_source(code: &str) -> (Vec<String>, Vec<String>) {
    debug_log!("In code_utils::find_use_renames_source");
    let use_as_regex: &Regex = re!(r"(?m)^\s*use\s+(\w+).*? as\s+(\w+)");

    let mut use_renames_from: Vec<String> = vec![];
    let mut use_renames_to: Vec<String> = vec![];

    for cap in use_as_regex.captures_iter(code) {
        let from_name = cap[1].to_string();
        let to_name = cap[2].to_string();

        debug_log!("use_rename: from={from_name}, to={to_name}");
        use_renames_from.push(from_name);
        use_renames_to.push(to_name);
    }

    use_renames_from.sort();
    use_renames_from.dedup();

    debug_log!("use_renames from source: from={use_renames_from:#?}, to={use_renames_to:#?}");
    (use_renames_from, use_renames_to)
}

/// Extract embedded Cargo.toml metadata from a Rust source string.
/// # Errors
/// Will return `Err` if there is any error in parsing the toml data into a manifest.
#[profiled]
pub fn extract(
    rs_full_source: &str,
    #[allow(unused_variables)] start_parsing_rs: Instant,
) -> ThagResult<Manifest> {
    let maybe_rs_toml = extract_toml_block(rs_full_source);

    profile!(parse, mem_summary, time);
    let mut rs_manifest = if let Some(rs_toml_str) = maybe_rs_toml {
        // debug_log!("rs_toml_str={rs_toml_str}");
        Manifest::from_str(&rs_toml_str)?
    } else {
        Manifest::from_str("")?
    };
    end!(parse);

    profile!(set_edition, mem_summary, time);
    if let Some(package) = rs_manifest.package.as_mut() {
        package.edition = cargo_toml::Inheritable::Set(Edition::E2021);
    }
    end!(set_edition);

    // debug_log!("rs_manifest={rs_manifest:#?}");

    #[cfg(debug_assertions)]
    debug_timings(&start_parsing_rs, "extract_manifest parsed source");
    Ok(rs_manifest)
}

/// Processes thag-auto dependencies in the manifest, replacing them with appropriate
/// dependency sources based on environment variables and context.
///
/// # Arguments
/// * `build_state` - Mutable reference to the build state containing the manifest
///
/// # Returns
/// * `ThagResult<()>` - Success or error result
///
/// # Errors
///
/// This function will bubble up any error returned by `resolve_thag_dependency`.
/// # Environment Variables
/// * `THAG_DEV_PATH` - Absolute path to local `thag_rs` development directory
/// * `THAG_GIT_REF` - Git reference (branch/tag/commit) for git dependencies
/// * `THAG_GIT_REPO` - Git repository URL (defaults to standard `thag_rs` repo)
/// * `CI` - Indicates CI environment
#[profiled]
pub fn process_thag_auto_dependencies(build_state: &mut BuildState) -> ThagResult<()> {
    if let Some(ref mut rs_manifest) = build_state.rs_manifest {
        let thag_crates = [
            "thag_common",
            "thag_rs",
            "thag_proc_macros",
            "thag_profiler",
            "thag_styling",
        ];

        for crate_name in &thag_crates {
            if let Some(dependency) = rs_manifest.dependencies.get(*crate_name) {
                if should_process_thag_auto(dependency) {
                    let new_dependency = resolve_thag_dependency(crate_name, dependency)?;
                    rs_manifest
                        .dependencies
                        .insert((*crate_name).to_string(), new_dependency);
                    build_state.thag_auto_processed = true;
                }
            }
            // Cater for windows target needing to avoid `color_detect` feature in the first instance.
            for target in rs_manifest.target.values_mut() {
                if let Some(dependency) = target.dependencies.get_mut(*crate_name) {
                    if should_process_thag_auto(dependency) {
                        *dependency = resolve_thag_dependency(crate_name, &dependency.clone())?;
                        // target
                        //     .dependencies
                        //     .insert((*crate_name).to_string(), new_dependency);
                        build_state.thag_auto_processed = true;
                    }
                }
            }
        }
    }
    Ok(())
}

/// Checks if a dependency has `thag-auto` enabled by looking for a version string
/// that contains "`thag-auto`" as a marker
fn should_process_thag_auto(dependency: &Dependency) -> bool {
    match dependency {
        Dependency::Detailed(detail) => {
            // Check if version contains our marker
            detail
                .version
                .as_ref()
                .is_some_and(|v| v.contains("thag-auto"))
        }
        Dependency::Simple(version) => version.contains("thag-auto"),
        Dependency::Inherited(_) => false,
    }
}

/// Resolves a thag dependency based on environment variables and context
fn resolve_thag_dependency(
    crate_name: &str,
    original_dep: &Dependency,
) -> ThagResult<cargo_toml::Dependency> {
    // Extract base dependency details, preserving features and other settings
    let (base_version, features, default_features) = match original_dep {
        Dependency::Detailed(detail) => {
            let version = detail.version.as_ref().and_then(|v| {
                // Extract real version from "version,thag-auto" format
                if v.contains("thag-auto") {
                    v.split(',').next().map(|s| s.trim().to_string())
                } else {
                    Some(v.clone())
                }
            });
            (version, detail.features.clone(), detail.default_features)
        }
        Dependency::Simple(version) => {
            let version = if version.contains("thag-auto") {
                version.split(',').next().map(|s| s.trim().to_string())
            } else {
                Some(version.clone())
            };
            (version, Vec::new(), true)
        }
        Dependency::Inherited(_) => return Ok(original_dep.clone()),
    };

    // Create new dependency detail with preserved settings
    let mut new_detail = Box::new(DependencyDetail {
        features,
        default_features,
        ..Default::default()
    });

    // Determine dependency source based on environment
    let is_ci = env::var("CI").is_ok();
    let git_ref_env_var = if is_ci { "GITHUB_REF" } else { "THAG_GIT_REF" };
    if let Ok(dev_path) = env::var("THAG_DEV_PATH") {
        // Development: use local path
        let crate_path = match crate_name {
            "thag_common" => format!("{}/thag_common", dev_path),
            "thag_proc_macros" => format!("{}/thag_proc_macros", dev_path),
            "thag_profiler" => format!("{}/thag_profiler", dev_path),
            "thag_styling" => format!("{}/thag_styling", dev_path),
            _ => dev_path,
        };

        new_detail.path = Some(crate_path);
        debug_log!("Using local path for {}: {:?}", crate_name, new_detail.path);
    } else if is_ci || env::var(git_ref_env_var).is_ok() {
        // CI or explicit git reference: use git dependency
        let git_repo = env::var("THAG_GIT_REPO")
            .unwrap_or_else(|_| "https://github.com/durbanlegend/thag_rs".to_string());
        let git_ref = env::var(git_ref_env_var).map_or_else(
            |_| "main".to_string(),
            |s| {
                let var_start = s.rfind('/').map_or_else(|| 0, |pos| pos + 1);
                s[var_start..].to_string()
            },
        );

        new_detail.git = Some(git_repo);
        new_detail.branch = Some(git_ref);
        debug_log!(
            "Using git dependency for {}: {:?} @ {:?}",
            crate_name,
            new_detail.git,
            new_detail.branch
        );
    } else {
        // Default: use crates.io version
        if let Some(version) = base_version {
            let query: Query = format!("{crate_name}@={version}")
                .parse()
                .map_err(|e| ThagError::FromStr(format!("Failed to parse query: {e}").into()))?;
            let result = query.submit();
            // .map_err(|e| ThagError::FromStr(format!("{e}").into()))?;
            // .map_err(|e| -> ThagError { format!("{e}").into() })?;
            if let Ok(Some(release)) = result {
                let vers = release.vers;
                new_detail.version = Some(format!("{}.{}.{}", vers.major, vers.minor, vers.patch));
                debug_log!(
                    "Using crates.io version for {crate_name}: {:?}",
                    new_detail.version
                );
            } else {
                display_thag_auto_help();
                return Err(ThagError::FromStr(
                    format!("{crate_name} version {} not found in crates.io", version).into(),
                ));
            }
        }
    }
    Ok(Dependency::Detailed(new_detail))
}

fn display_thag_auto_help() {
    svprtln!(
        Role::ERR,
        V::N,
        "Build failed - thag dependency issue detected"
    );
    svprtln!(
        Role::EMPH,
        V::N,
        r"
This script uses thag dependencies (thag_common, thag_rs, thag_proc_macros, thag_profiler or thag_styling)
with the 'thag-auto' keyword, which automatically resolves to the appropriate
dependency source based on your environment.

The most likely issue is that the version specified in the script doesn't exist
on crates.io yet. To fix this, you have several options:

1. DEVELOPMENT (recommended): Set environment variable to use local path
    {}, e.g.:

   {}

2. GIT DEPENDENCY: Use git reference to get the latest version
   export THAG_GIT_REF=main

3. ALWAYS RUN THROUGH THAG: Use 'thag script.rs' instead of 'cargo build'
   (This allows thag-auto processing to work properly)

The thag-auto system is designed to work with crates.io by default, falling back
to git or local paths when environment variables are set. This allows the same
script to work in different environments without modification.

For more details, see the comments in demo scripts or the thag documentation.",
        if cfg!(target_os = "windows") {
            "(Assuming PowerShell:) $env:THAG_DEV_PATH = absolute\\path\\to\\thag_rs"
        } else {
            "export THAG_DEV_PATH=/absolute/path/to/thag_rs"
        },
        styled!(
            if cfg!(target_os = "windows") {
                "$env:THAG_DEV_PATH = $PWD"
            } else {
                "export THAG_DEV_PATH=$PWD"
            },
            bold,
            reversed
        )
    );
}

#[profiled]
fn extract_toml_block(input: &str) -> Option<String> {
    let re: &Regex = re!(r"(?s)/\*\[toml\](.*?)\*/");
    re.captures(input)
        .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}

/// Extract the `use` statements from source and parse them to a `syn::File` in order to
/// extract the dependencies..
///
/// # Errors
///
/// This function will return an error if `syn` fails to parse the `use` statements as a `syn::File`.
#[profiled]
pub fn extract_and_wrap_uses(source: &str) -> Result<Ast, syn::Error> {
    // Step 1: Capture `use` statements
    let use_simple_regex: &Regex = re!(r"(?m)(^\s*use\s+[^;{]+;\s*$)");
    let use_nested_regex: &Regex = re!(r"(?ms)(^\s*use\s+\{.*\};\s*$)");

    let mut use_statements: Vec<String> = vec![];

    for cap in use_simple_regex.captures_iter(source) {
        let use_string = cap[1].to_string();
        use_statements.push(use_string);
    }
    for cap in use_nested_regex.captures_iter(source) {
        let use_string = cap[1].to_string();
        use_statements.push(use_string);
    }

    // Step 2: Parse as `syn::File`
    let ast: File = parse_file(&use_statements.join("\n"))?;
    // eprintln!("ast={ast:#?}");

    // Return wrapped in `Ast::File`
    Ok(Ast::File(ast))
}

#[profiled]
fn clean_features(features: Vec<String>) -> Vec<String> {
    let mut features: Vec<String> = features
        .into_iter()
        .filter(|f| !f.contains('/')) // Filter out features with slashes
        .collect();
    features.sort();
    features
}

#[profiled]
fn get_crate_features(name: &str) -> Option<Vec<String>> {
    let query: Query = match name.parse() {
        Ok(q) => q,
        Err(e) => {
            debug_log!("Failed to parse query for crate {}: {}", name, e);
            return None;
        }
    };

    match query.package() {
        Ok(package) => {
            let latest = package.into_latest()?;

            // Collect features from both fields
            let mut all_features: Vec<String> = latest.features.keys().cloned().collect();

            // Add features2 if present
            if let Some(features2) = latest.features2 {
                all_features.extend(features2.keys().cloned());
            }

            if all_features.is_empty() {
                None
            } else {
                Some(clean_features(all_features))
            }
        }
        Err(e) => {
            debug_log!("Failed to get features for crate {}: {}", name, e);
            None
        }
    }
}

/// Look up dependencies and add them to the manifest's dependency map.
///
/// This function takes a list of inferred dependency names from Rust source code
/// and attempts to look up their versions using cargo lookup. Based on the
/// inference level, it will add basic dependencies or include features.
///
/// # Arguments
/// * `inference_level` - The level of dependency inference to perform
/// * `rs_inferred_deps` - List of dependency names inferred from source code
/// * `rs_dep_map` - Mutable reference to the dependency map to populate
#[allow(clippy::missing_panics_doc)]
#[profiled]
pub fn lookup_deps(
    inference_level: &DependencyInference,
    rs_inferred_deps: &[String],
    rs_dep_map: &mut BTreeMap<String, Dependency>,
) {
    if rs_inferred_deps.is_empty() {
        return;
    }

    let existing_toml_block = !&rs_dep_map.is_empty();
    let mut new_inferred_deps: Vec<String> = vec![];
    let recomm_style = &Style::for_role(Role::Heading1);
    let recomm_inf_level = &DependencyInference::Config;
    let actual_style = if inference_level == recomm_inf_level {
        recomm_style
    } else {
        &Style::for_role(Role::Emphasis)
    };
    let styled_inference_level = actual_style.paint(inference_level.to_string());
    let styled_recomm_inf_level = recomm_style.paint(recomm_inf_level.to_string());
    // Hack: use reset string \x1b[0m here to avoid mystery white-on-white bug.
    svprtln!(
        Role::NORM,
        V::V,
        "\x1b[0mRecommended dependency inference_level={styled_recomm_inf_level}, actual={styled_inference_level}"
    );

    let config = maybe_config();
    let binding = Dependencies::default();
    let dep_config = config.as_ref().map_or(&binding, |c| &c.dependencies);
    for dep_name in rs_inferred_deps {
        if dep_name == "thag_demo_proc_macros" {
            proc_macros_magic(rs_dep_map, dep_name, "demo");
            continue;
        } else if dep_name == "thag_bank_proc_macros" {
            proc_macros_magic(rs_dep_map, dep_name, "bank");
            continue;
        } else if rs_dep_map.contains_key(dep_name) {
            continue;
        }

        if let Some((name, version)) = cargo_lookup(dep_name) {
            if rs_dep_map.contains_key(&name) || rs_dep_map.contains_key(dep_name.as_str()) {
                continue;
            }
            // Only do it after lookup in case the found crate name has hyphens instead of the underscores it has in code.
            new_inferred_deps.push(name.clone());
            let features = get_crate_features(&name);

            match inference_level {
                DependencyInference::None => {
                    // Skip dependency entirely
                }
                DependencyInference::Min => {
                    // Just add basic dependency
                    insert_simple(rs_dep_map, name, version);
                }
                DependencyInference::Config | DependencyInference::Max => {
                    // eprintln!("crate={name}, features.is_some()? {}", features.is_some());
                    if let Some(ref all_features) = features {
                        let features_for_inference_level = dep_config
                            .get_features_for_inference_level(&name, all_features, inference_level);
                        // eprintln!("features_for_inference_level={features_for_inference_level:#?}");
                        if let (Some(final_features), default_features) =
                            features_for_inference_level
                        {
                            rs_dep_map.entry(name.clone()).or_insert_with(|| {
                                Dependency::Detailed(Box::new(DependencyDetail {
                                    version: Some(version.clone()),
                                    features: final_features,
                                    default_features,
                                    ..Default::default()
                                }))
                            });
                        } else {
                            insert_simple(rs_dep_map, name, version);
                        }
                    } else {
                        insert_simple(rs_dep_map, name, version);
                    }
                }
            }
        }
    }

    if get_verbosity() < V::V
        || matches!(inference_level, DependencyInference::None)
        || new_inferred_deps.is_empty()
    {
        // No generated manifest info to show.
        return;
    }
    display_toml_info(
        existing_toml_block,
        &new_inferred_deps,
        rs_dep_map,
        inference_level,
    );
}

#[profiled]
fn insert_simple(rs_dep_map: &mut BTreeMap<String, Dependency>, name: String, version: String) {
    rs_dep_map
        .entry(name)
        .or_insert_with(|| Dependency::Simple(version));
}

#[profiled]
fn display_toml_info(
    existing_toml_block: bool,
    new_inferred_deps: &[String],
    rs_dep_map: &BTreeMap<String, Dependency>,
    inference_level: &DependencyInference,
) {
    let mut toml_block = String::new();
    if !existing_toml_block {
        toml_block.push_str("/*[toml]\n[dependencies]\n");
    }
    for dep_name in new_inferred_deps {
        // eprintln!("dep_name={dep_name}");
        let value = rs_dep_map.get(dep_name);
        match value {
            Some(Dependency::Simple(string)) => {
                let dep_line = format!("{dep_name} = \"{string}\"\n");
                toml_block.push_str(&dep_line);
            }
            Some(Dependency::Detailed(dep)) => {
                if dep.features.is_empty() {
                    let dep_line = format!(
                        "{dep_name} = \"{}\"\n",
                        dep.version
                            .as_ref()
                            .unwrap_or_else(|| panic!("Error unwrapping version for {dep_name}")),
                    );
                    toml_block.push_str(&dep_line);
                } else {
                    let maybe_default_features = if dep.default_features {
                        ""
                    } else {
                        ", default-features = false"
                    };
                    let dep_line = format!(
                        "{} = {{ version = \"{}\"{maybe_default_features}, features = [{}] }}\n",
                        dep_name,
                        dep.version
                            .as_ref()
                            .unwrap_or_else(|| panic!("Error unwrapping version for {dep_name}")),
                        dep.features
                            .iter()
                            .map(|f| format!("\"{}\"", f))
                            .collect::<Vec<_>>()
                            .join(", ")
                    );

                    toml_block.push_str(&dep_line);
                }
            }
            Some(Dependency::Inherited(_)) | None => (),
        }
    }
    if !existing_toml_block {
        toml_block.push_str("*/");
    }
    let styled_toml_block = Style::for_role(Role::Heading2).paint(&toml_block);
    let styled_inference_level = Style::for_role(Role::EMPH).paint(inference_level.to_string());
    let wording = if existing_toml_block {
        format!("This is the {styled_inference_level} manifest information that was generated for this run. If you want to, you can merge it into the existing toml block at")
    } else {
        format!("This toml block contains the same {styled_inference_level} manifest information that was generated for this run. If you want to, you can copy it into")
    };
    vprtln!(
        V::N,
        "\n{wording} the top of your script, to lock it down or maybe compile a little faster in future:\n{styled_toml_block}\n"
    );
}

#[profiled]
fn proc_macros_magic(
    rs_dep_map: &mut BTreeMap<String, Dependency>,
    dep_name: &str,
    dir_name: &str,
) {
    svprtln!(
        Role::INFO,
        V::V,
        r#"Found magic import `{dep_name}`: attempting to generate path dependency from `proc_macros.(...)proc_macro_crate_path` in config file ".../config.toml"."#
    );
    let default_proc_macros_dir = format!("{dir_name}/proc_macros");
    let maybe_magic_proc_macros_dir = maybe_config().map_or_else(
        || {
            debug_log!(
                r#"Missing config file for "use {dep_name};", defaulting to "{dir_name}/proc_macros"."#
            );
            Some(default_proc_macros_dir.clone())
        },
        |config| {
            debug_log!("Found config.proc_macros={:#?}", config.proc_macros);
            if dep_name == "thag_demo_proc_macros" {
                config.proc_macros.demo_proc_macro_crate_path
            } else if dep_name == "thag_bank_proc_macros" {
                config.proc_macros.bank_proc_macro_crate_path
            } else {
                None
            }
        },
    );
    let magic_proc_macros_dir = maybe_magic_proc_macros_dir.as_ref().map_or_else(|| {
        svprtln!(
            Role::INFO,
            V::V,
            r#"No `config.proc_macros.proc_macro_crate_path` in config file for "use {dep_name};": defaulting to "{default_proc_macros_dir}"."#
        );
        default_proc_macros_dir
    }, |proc_macros_dir| {
        svprtln!(Role::INFO, V::V, "Found {proc_macros_dir:#?}.");
        proc_macros_dir.to_string()
    });

    let path = PathBuf::from_str(&magic_proc_macros_dir).unwrap();
    let path = if path.is_absolute() {
        path
    } else {
        path.canonicalize()
            .unwrap_or_else(|_| panic!("Could not canonicalize path {}", path.display()))
    };
    let dep = Dependency::Detailed(Box::new(DependencyDetail {
        path: Some(path.display().to_string()),
        ..Default::default()
    }));
    rs_dep_map.insert(dep_name.to_string(), dep);
}

/// Identify mod statements for exclusion from Cargo.toml metadata.
/// Fallback version for when an abstract syntax tree cannot be parsed.
#[must_use]
#[profiled]
pub fn find_modules_source(code: &str) -> Vec<String> {
    let module_regex: &Regex = re!(r"(?m)^[\s]*mod\s+([^;{\s]+)");
    debug_log!("In code_utils::find_use_renames_source");
    let mut modules: Vec<String> = vec![];
    for cap in module_regex.captures_iter(code) {
        let module = cap[1].to_string();
        debug_log!("module={module}");
        modules.push(module);
    }
    debug_log!("modules from source={modules:#?}");
    modules
}