Skip to main content

zccache_depgraph/
rustc_args.rs

1//! Rustc argument parser for cache key computation.
2//!
3//! Extracts cache-relevant flags from rustc command lines. Separates
4//! args that affect compilation output (included in cache key) from
5//! args that are cosmetic or path-dependent (excluded).
6
7use std::path::Path;
8
9use zccache_core::NormalizedPath;
10
11/// A parsed `--extern name=path` declaration.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ExternCrate {
14    /// The crate name (e.g., "serde").
15    pub name: String,
16    /// Path to the rlib/rmeta file.
17    pub path: NormalizedPath,
18}
19
20/// Result of parsing rustc arguments for cache key computation.
21#[derive(Debug, Clone)]
22pub struct RustcParsedArgs {
23    /// The source file (positional .rs arg).
24    pub source_file: NormalizedPath,
25
26    // ── Cache-key fields (affect compilation output) ──
27    /// `--crate-name` value.
28    pub crate_name: Option<String>,
29    /// `--crate-type` values (lib, rlib, staticlib).
30    pub crate_types: Vec<String>,
31    /// `--edition` value (2015, 2018, 2021, 2024).
32    pub edition: Option<String>,
33    /// `--emit` types (dep-info, metadata, link, etc.).
34    pub emit_types: Vec<String>,
35    /// `--cfg` values (sorted for deterministic hashing).
36    pub cfgs: Vec<String>,
37    /// `--check-cfg` values (sorted).
38    pub check_cfgs: Vec<String>,
39    /// Cache-relevant `-C` codegen options (sorted).
40    /// Includes: opt-level, codegen-units, target-cpu, target-feature,
41    /// lto, panic, debuginfo, strip, overflow-checks, embed-bitcode.
42    /// Excludes: incremental and linker/pass-through options. Cargo metadata
43    /// and extra-filename are tracked separately because they affect rustc
44    /// artifact identity and output names.
45    pub codegen_flags: Vec<String>,
46    /// `--target` value (cross-compilation triple).
47    pub target: Option<String>,
48    /// `--cap-lints` value.
49    pub cap_lints: Option<String>,
50    /// `--extern` crate declarations (name + path for content hashing).
51    pub externs: Vec<ExternCrate>,
52    /// Lint flags: `-A`, `-W`, `-D`, `-F` (sorted).
53    pub lint_flags: Vec<String>,
54    /// Flags not recognized by the parser (sorted, hashed into key).
55    pub unknown_flags: Vec<String>,
56
57    // ── Non-cache-key fields (needed for output path / depfile) ──
58    /// `--out-dir` path.
59    pub out_dir: Option<NormalizedPath>,
60    /// `-C extra-filename=` value.
61    pub extra_filename: Option<String>,
62    /// `-C metadata=` value (cargo's disambiguation hash).
63    pub cargo_metadata: Option<String>,
64    /// `-C incremental=` path.
65    pub incremental_dir: Option<NormalizedPath>,
66    /// `--error-format` value.
67    pub error_format: Option<String>,
68    /// `--json` value.
69    pub json_format: Option<String>,
70    /// `--color` value.
71    pub color: Option<String>,
72    /// `--diagnostic-width` value.
73    pub diagnostic_width: Option<String>,
74    /// `-L` search paths.
75    pub search_paths: Vec<NormalizedPath>,
76    /// `--remap-path-prefix` values.
77    pub remap_path_prefixes: Vec<String>,
78    /// `--sysroot` path.
79    pub sysroot: Option<NormalizedPath>,
80    /// `-o` output file (explicit).
81    pub output_file: Option<NormalizedPath>,
82}
83
84/// Codegen options excluded from cache key (cosmetic or path-dependent).
85/// Any `-C` option NOT in this list is included in the cache key by default,
86/// which is the safe choice: unknown options are assumed to affect output.
87const EXCLUDED_CODEGEN: &[&str] = &[
88    "incremental",
89    "linker",
90    "link-arg",
91    "link-args",
92    "save-temps",
93    "remark",
94];
95
96/// Parse rustc arguments into structured form for cache key computation.
97///
98/// `args` should be the arguments after the compiler executable.
99/// Relative paths are resolved against `cwd`.
100pub fn parse_rustc_args(args: &[String], cwd: &Path) -> RustcParsedArgs {
101    let mut result = RustcParsedArgs {
102        source_file: NormalizedPath::new(""),
103        crate_name: None,
104        crate_types: Vec::new(),
105        edition: None,
106        emit_types: Vec::new(),
107        cfgs: Vec::new(),
108        check_cfgs: Vec::new(),
109        codegen_flags: Vec::new(),
110        target: None,
111        cap_lints: None,
112        externs: Vec::new(),
113        lint_flags: Vec::new(),
114        unknown_flags: Vec::new(),
115        out_dir: None,
116        extra_filename: None,
117        cargo_metadata: None,
118        incremental_dir: None,
119        error_format: None,
120        json_format: None,
121        color: None,
122        diagnostic_width: None,
123        search_paths: Vec::new(),
124        remap_path_prefixes: Vec::new(),
125        sysroot: None,
126        output_file: None,
127    };
128
129    let mut i = 0;
130    while i < args.len() {
131        let arg = &args[i];
132
133        // --edition <val> or --edition=<val>
134        if let Some(val) = take_option(arg, "--edition", args.get(i + 1), &mut i) {
135            result.edition = Some(val);
136            continue;
137        }
138
139        // --crate-type <val> or --crate-type=<val>
140        // Rustc accepts comma-separated types: --crate-type lib,rlib
141        if let Some(val) = take_option(arg, "--crate-type", args.get(i + 1), &mut i) {
142            result
143                .crate_types
144                .extend(val.split(',').map(|s| s.to_string()));
145            continue;
146        }
147
148        // --crate-name <val> or --crate-name=<val>
149        if let Some(val) = take_option(arg, "--crate-name", args.get(i + 1), &mut i) {
150            result.crate_name = Some(val);
151            continue;
152        }
153
154        // --emit <types> or --emit=<types>
155        if let Some(val) = take_option(arg, "--emit", args.get(i + 1), &mut i) {
156            for part in val.split(',') {
157                // Handle --emit=dep-info=path/to/file form
158                let emit_type = part.split('=').next().unwrap_or(part).to_string();
159                if !result.emit_types.contains(&emit_type) {
160                    result.emit_types.push(emit_type);
161                }
162            }
163            continue;
164        }
165
166        // --target <val> or --target=<val>
167        if let Some(val) = take_option(arg, "--target", args.get(i + 1), &mut i) {
168            result.target = Some(val);
169            continue;
170        }
171
172        // --cap-lints <val>
173        if let Some(val) = take_option(arg, "--cap-lints", args.get(i + 1), &mut i) {
174            result.cap_lints = Some(val);
175            continue;
176        }
177
178        // --cfg <val> or --cfg=<val>
179        if let Some(val) = take_option(arg, "--cfg", args.get(i + 1), &mut i) {
180            result.cfgs.push(val);
181            continue;
182        }
183
184        // --check-cfg <val> or --check-cfg=<val>
185        if let Some(val) = take_option(arg, "--check-cfg", args.get(i + 1), &mut i) {
186            result.check_cfgs.push(val);
187            continue;
188        }
189
190        // --extern <name=path> or --extern=<name=path>
191        if let Some(val) = take_option(arg, "--extern", args.get(i + 1), &mut i) {
192            if let Some((name, path)) = val.split_once('=') {
193                // Handle noprelude:name=path form
194                let actual_name = name.strip_prefix("noprelude:").unwrap_or(name);
195                result.externs.push(ExternCrate {
196                    name: actual_name.to_string(),
197                    path: resolve_path(path, cwd),
198                });
199            }
200            // --extern name (without =path) — no file to hash
201            continue;
202        }
203
204        // --out-dir <path> or --out-dir=<path>
205        if let Some(val) = take_option(arg, "--out-dir", args.get(i + 1), &mut i) {
206            result.out_dir = Some(resolve_path(&val, cwd));
207            continue;
208        }
209
210        // --error-format <val>
211        if let Some(val) = take_option(arg, "--error-format", args.get(i + 1), &mut i) {
212            result.error_format = Some(val);
213            continue;
214        }
215
216        // --json <val>
217        if let Some(val) = take_option(arg, "--json", args.get(i + 1), &mut i) {
218            result.json_format = Some(val);
219            continue;
220        }
221
222        // --color <val>
223        if let Some(val) = take_option(arg, "--color", args.get(i + 1), &mut i) {
224            result.color = Some(val);
225            continue;
226        }
227
228        // --diagnostic-width <val>
229        if let Some(val) = take_option(arg, "--diagnostic-width", args.get(i + 1), &mut i) {
230            result.diagnostic_width = Some(val);
231            continue;
232        }
233
234        // --sysroot <path>
235        if let Some(val) = take_option(arg, "--sysroot", args.get(i + 1), &mut i) {
236            result.sysroot = Some(resolve_path(&val, cwd));
237            continue;
238        }
239
240        // --remap-path-prefix <val>
241        if let Some(val) = take_option(arg, "--remap-path-prefix", args.get(i + 1), &mut i) {
242            result.remap_path_prefixes.push(val);
243            continue;
244        }
245
246        // --env-set <val> — skip (nightly feature, not cache-relevant)
247        if let Some(_val) = take_option(arg, "--env-set", args.get(i + 1), &mut i) {
248            continue;
249        }
250
251        // -o <path>
252        if arg == "-o" {
253            if let Some(next) = args.get(i + 1) {
254                result.output_file = Some(resolve_path(next, cwd));
255                i += 2;
256                continue;
257            }
258        }
259
260        // -L <path>
261        if arg == "-L" {
262            if let Some(next) = args.get(i + 1) {
263                // -L [KIND=]PATH — strip the kind= prefix
264                let path_str = next.split_once('=').map(|(_, p)| p).unwrap_or(next);
265                result.search_paths.push(resolve_path(path_str, cwd));
266                i += 2;
267                continue;
268            }
269        } else if let Some(rest) = arg.strip_prefix("-L") {
270            if !rest.is_empty() {
271                let path_str = rest.split_once('=').map(|(_, p)| p).unwrap_or(rest);
272                result.search_paths.push(resolve_path(path_str, cwd));
273                i += 1;
274                continue;
275            }
276        }
277
278        // -C <option> or --codegen <option>
279        if arg == "-C" || arg == "--codegen" {
280            if let Some(next) = args.get(i + 1) {
281                handle_codegen_option(next, cwd, &mut result);
282                i += 2;
283                continue;
284            }
285        } else if let Some(rest) = arg.strip_prefix("-C") {
286            if !rest.is_empty() {
287                handle_codegen_option(rest, cwd, &mut result);
288                i += 1;
289                continue;
290            }
291        }
292
293        // Lint flags: -A, -W, -D, -F
294        if matches!(arg.as_str(), "-A" | "-W" | "-D" | "-F") {
295            if let Some(next) = args.get(i + 1) {
296                result.lint_flags.push(format!("{arg} {next}"));
297                i += 2;
298                continue;
299            }
300        }
301
302        // -Z <option> — nightly flags. Consume both flag and value.
303        if arg == "-Z" {
304            if let Some(next) = args.get(i + 1) {
305                result.unknown_flags.push(format!("-Z {next}"));
306                i += 2;
307                continue;
308            }
309        }
310
311        // Any flag starting with -
312        if arg.starts_with('-') {
313            result.unknown_flags.push(arg.clone());
314            i += 1;
315            continue;
316        }
317
318        // Positional arg — source file
319        if arg.ends_with(".rs") {
320            result.source_file = resolve_path(arg, cwd);
321        }
322
323        i += 1;
324    }
325
326    // Sort all collections for deterministic hashing
327    result.cfgs.sort();
328    result.check_cfgs.sort();
329    result.codegen_flags.sort();
330    result.lint_flags.sort();
331    result.unknown_flags.sort();
332
333    result
334}
335
336/// Try to extract a `--flag value` or `--flag=value` option.
337/// Returns the value and advances `i` appropriately.
338fn take_option(arg: &str, flag: &str, next: Option<&String>, i: &mut usize) -> Option<String> {
339    if arg == flag {
340        if let Some(next_val) = next {
341            *i += 2;
342            return Some(next_val.clone());
343        }
344    } else if let Some(val) = arg.strip_prefix(&format!("{flag}=")) {
345        *i += 1;
346        return Some(val.to_string());
347    }
348    None
349}
350
351/// Process a `-C <option>` codegen flag.
352fn handle_codegen_option(opt: &str, cwd: &Path, result: &mut RustcParsedArgs) {
353    let (key, value) = opt.split_once('=').unwrap_or((opt, ""));
354
355    // Excluded codegen options (not cache-relevant)
356    if key == "metadata" {
357        result.cargo_metadata = Some(value.to_string());
358        return;
359    }
360    if key == "extra-filename" {
361        result.extra_filename = Some(value.to_string());
362        return;
363    }
364    if key == "incremental" {
365        result.incremental_dir = Some(resolve_path(value, cwd));
366        return;
367    }
368    if EXCLUDED_CODEGEN.contains(&key) {
369        return;
370    }
371
372    // Cache-relevant codegen options
373    result.codegen_flags.push(opt.to_string());
374}
375
376/// Resolve a path against cwd if relative.
377fn resolve_path(path: &str, cwd: &Path) -> NormalizedPath {
378    let p = Path::new(path);
379    if p.is_absolute() {
380        NormalizedPath::new(p)
381    } else {
382        NormalizedPath::new(cwd.join(p))
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use zccache_core::NormalizedPath;
389
390    use super::*;
391
392    fn args(s: &[&str]) -> Vec<String> {
393        s.iter().map(|x| x.to_string()).collect()
394    }
395
396    fn cwd() -> NormalizedPath {
397        NormalizedPath::from("/project")
398    }
399
400    #[test]
401    fn basic_parse_source_file() {
402        let parsed = parse_rustc_args(&args(&["src/lib.rs"]), &cwd());
403        assert_eq!(
404            parsed.source_file,
405            NormalizedPath::from("/project/src/lib.rs")
406        );
407    }
408
409    #[test]
410    fn parse_edition() {
411        let parsed = parse_rustc_args(&args(&["--edition", "2021", "src/lib.rs"]), &cwd());
412        assert_eq!(parsed.edition.as_deref(), Some("2021"));
413    }
414
415    #[test]
416    fn parse_edition_equals_form() {
417        let parsed = parse_rustc_args(&args(&["--edition=2021", "src/lib.rs"]), &cwd());
418        assert_eq!(parsed.edition.as_deref(), Some("2021"));
419    }
420
421    #[test]
422    fn parse_crate_type() {
423        let parsed = parse_rustc_args(
424            &args(&["--crate-type", "lib", "--crate-type", "rlib", "src/lib.rs"]),
425            &cwd(),
426        );
427        assert_eq!(parsed.crate_types, vec!["lib", "rlib"]);
428    }
429
430    #[test]
431    fn parse_crate_name() {
432        let parsed = parse_rustc_args(&args(&["--crate-name", "mylib", "src/lib.rs"]), &cwd());
433        assert_eq!(parsed.crate_name.as_deref(), Some("mylib"));
434    }
435
436    #[test]
437    fn parse_emit_types() {
438        let parsed = parse_rustc_args(
439            &args(&["--emit=dep-info,metadata,link", "src/lib.rs"]),
440            &cwd(),
441        );
442        assert_eq!(parsed.emit_types, vec!["dep-info", "metadata", "link"]);
443    }
444
445    #[test]
446    fn parse_emit_with_paths() {
447        // --emit=dep-info=/path/to/deps.d,metadata,link
448        let parsed = parse_rustc_args(
449            &args(&["--emit=dep-info=/tmp/deps.d,metadata,link", "src/lib.rs"]),
450            &cwd(),
451        );
452        assert_eq!(parsed.emit_types, vec!["dep-info", "metadata", "link"]);
453    }
454
455    #[test]
456    fn parse_cfg_values() {
457        let parsed = parse_rustc_args(
458            &args(&["--cfg", "feature=\"derive\"", "--cfg", "unix", "src/lib.rs"]),
459            &cwd(),
460        );
461        // Sorted
462        assert_eq!(parsed.cfgs, vec!["feature=\"derive\"", "unix"]);
463    }
464
465    #[test]
466    fn parse_codegen_flags() {
467        let parsed = parse_rustc_args(
468            &args(&["-C", "opt-level=2", "-C", "debuginfo=2", "src/lib.rs"]),
469            &cwd(),
470        );
471        // Sorted
472        assert!(parsed.codegen_flags.contains(&"debuginfo=2".to_string()));
473        assert!(parsed.codegen_flags.contains(&"opt-level=2".to_string()));
474    }
475
476    #[test]
477    fn parse_codegen_concatenated() {
478        let parsed = parse_rustc_args(&args(&["-Copt-level=3", "src/lib.rs"]), &cwd());
479        assert!(parsed.codegen_flags.contains(&"opt-level=3".to_string()));
480    }
481
482    #[test]
483    fn excluded_codegen_not_in_cache_key() {
484        let parsed = parse_rustc_args(
485            &args(&[
486                "-C",
487                "metadata=abc123",
488                "-C",
489                "extra-filename=-abc123",
490                "-C",
491                "incremental=/tmp/incr",
492                "-C",
493                "linker=cc",
494                "src/lib.rs",
495            ]),
496            &cwd(),
497        );
498        // None of these should be in codegen_flags
499        assert!(parsed.codegen_flags.is_empty());
500        // But they should be in their dedicated fields
501        assert_eq!(parsed.cargo_metadata.as_deref(), Some("abc123"));
502        assert_eq!(parsed.extra_filename.as_deref(), Some("-abc123"));
503        assert_eq!(
504            parsed.incremental_dir,
505            Some(NormalizedPath::from("/tmp/incr"))
506        );
507    }
508
509    #[test]
510    fn parse_extern_crates() {
511        let parsed = parse_rustc_args(
512            &args(&[
513                "--extern",
514                "serde=/target/deps/libserde.rlib",
515                "--extern",
516                "log=/target/deps/liblog.rmeta",
517                "src/lib.rs",
518            ]),
519            &cwd(),
520        );
521        assert_eq!(parsed.externs.len(), 2);
522        assert_eq!(parsed.externs[0].name, "serde");
523        assert_eq!(
524            parsed.externs[0].path,
525            NormalizedPath::from("/target/deps/libserde.rlib")
526        );
527        assert_eq!(parsed.externs[1].name, "log");
528    }
529
530    #[test]
531    fn parse_extern_noprelude() {
532        let parsed = parse_rustc_args(
533            &args(&[
534                "--extern",
535                "noprelude:core=/path/libcore.rlib",
536                "src/lib.rs",
537            ]),
538            &cwd(),
539        );
540        assert_eq!(parsed.externs[0].name, "core");
541    }
542
543    #[test]
544    fn search_paths_excluded_from_cache_key() {
545        let parsed = parse_rustc_args(
546            &args(&[
547                "-L",
548                "dependency=/target/deps",
549                "-L",
550                "native=/usr/lib",
551                "src/lib.rs",
552            ]),
553            &cwd(),
554        );
555        assert_eq!(parsed.search_paths.len(), 2);
556        // search_paths are stored but NOT in codegen_flags/cfgs/unknown_flags
557        assert!(parsed.codegen_flags.is_empty());
558        assert!(parsed.unknown_flags.is_empty());
559    }
560
561    #[test]
562    fn out_dir_excluded_from_cache_key() {
563        let parsed = parse_rustc_args(
564            &args(&["--out-dir", "/target/debug/deps", "src/lib.rs"]),
565            &cwd(),
566        );
567        assert_eq!(
568            parsed.out_dir,
569            Some(NormalizedPath::from("/target/debug/deps"))
570        );
571        assert!(parsed.unknown_flags.is_empty());
572    }
573
574    #[test]
575    fn cosmetic_flags_excluded() {
576        let parsed = parse_rustc_args(
577            &args(&[
578                "--error-format=json",
579                "--json=diagnostic-rendered-ansi",
580                "--color=always",
581                "--diagnostic-width=80",
582                "src/lib.rs",
583            ]),
584            &cwd(),
585        );
586        assert_eq!(parsed.error_format.as_deref(), Some("json"));
587        assert_eq!(
588            parsed.json_format.as_deref(),
589            Some("diagnostic-rendered-ansi")
590        );
591        assert_eq!(parsed.color.as_deref(), Some("always"));
592        assert_eq!(parsed.diagnostic_width.as_deref(), Some("80"));
593        // None of these should be in unknown_flags
594        assert!(parsed.unknown_flags.is_empty());
595    }
596
597    #[test]
598    fn parse_target() {
599        let parsed = parse_rustc_args(
600            &args(&["--target", "x86_64-unknown-linux-gnu", "src/lib.rs"]),
601            &cwd(),
602        );
603        assert_eq!(parsed.target.as_deref(), Some("x86_64-unknown-linux-gnu"));
604    }
605
606    #[test]
607    fn parse_cap_lints() {
608        let parsed = parse_rustc_args(&args(&["--cap-lints", "allow", "src/lib.rs"]), &cwd());
609        assert_eq!(parsed.cap_lints.as_deref(), Some("allow"));
610    }
611
612    #[test]
613    fn parse_lint_flags() {
614        let parsed = parse_rustc_args(
615            &args(&[
616                "-A",
617                "dead_code",
618                "-W",
619                "unused",
620                "-D",
621                "warnings",
622                "src/lib.rs",
623            ]),
624            &cwd(),
625        );
626        assert_eq!(parsed.lint_flags.len(), 3);
627        assert!(parsed.lint_flags.contains(&"-A dead_code".to_string()));
628        assert!(parsed.lint_flags.contains(&"-D warnings".to_string()));
629        assert!(parsed.lint_flags.contains(&"-W unused".to_string()));
630    }
631
632    #[test]
633    fn parse_output_file() {
634        let parsed = parse_rustc_args(&args(&["-o", "libfoo.rlib", "src/lib.rs"]), &cwd());
635        assert_eq!(
636            parsed.output_file,
637            Some(NormalizedPath::from("/project/libfoo.rlib"))
638        );
639    }
640
641    #[test]
642    fn full_cargo_invocation() {
643        let parsed = parse_rustc_args(
644            &args(&[
645                "--edition",
646                "2021",
647                "--crate-type",
648                "lib",
649                "--crate-name",
650                "serde",
651                "--emit=dep-info,metadata,link",
652                "-C",
653                "opt-level=2",
654                "-C",
655                "metadata=abc123def",
656                "-C",
657                "extra-filename=-abc123def",
658                "--out-dir",
659                "/target/release/deps",
660                "-L",
661                "dependency=/target/release/deps",
662                "--extern",
663                "serde_derive=/target/release/deps/libserde_derive-xyz.so",
664                "--cap-lints",
665                "allow",
666                "--cfg",
667                "feature=\"derive\"",
668                "--cfg",
669                "feature=\"std\"",
670                "--error-format=json",
671                "--json=diagnostic-rendered-ansi,artifacts,future-incompat",
672                "--diagnostic-width=211",
673                "-C",
674                "linker=cc",
675                "src/lib.rs",
676            ]),
677            &cwd(),
678        );
679
680        // Cache-key fields populated
681        assert_eq!(parsed.edition.as_deref(), Some("2021"));
682        assert_eq!(parsed.crate_types, vec!["lib"]);
683        assert_eq!(parsed.crate_name.as_deref(), Some("serde"));
684        assert_eq!(parsed.emit_types, vec!["dep-info", "metadata", "link"]);
685        assert!(parsed.codegen_flags.contains(&"opt-level=2".to_string()));
686        assert_eq!(parsed.cap_lints.as_deref(), Some("allow"));
687        assert!(parsed.cfgs.contains(&"feature=\"derive\"".to_string()));
688        assert!(parsed.cfgs.contains(&"feature=\"std\"".to_string()));
689        assert_eq!(parsed.externs.len(), 1);
690        assert_eq!(parsed.externs[0].name, "serde_derive");
691
692        // Excluded fields populated but NOT in cache-key collections
693        assert_eq!(parsed.cargo_metadata.as_deref(), Some("abc123def"));
694        assert_eq!(parsed.extra_filename.as_deref(), Some("-abc123def"));
695        assert_eq!(parsed.error_format.as_deref(), Some("json"));
696        assert!(parsed.search_paths.len() == 1);
697        assert!(parsed.unknown_flags.is_empty());
698    }
699
700    #[test]
701    fn z_flag_with_value_captured() {
702        let parsed = parse_rustc_args(
703            &args(&["-Z", "macro-backtrace", "--crate-type", "lib", "src/lib.rs"]),
704            &cwd(),
705        );
706        // -Z and its value should be combined into one entry
707        assert!(
708            parsed
709                .unknown_flags
710                .contains(&"-Z macro-backtrace".to_string()),
711            "got: {:?}",
712            parsed.unknown_flags
713        );
714    }
715
716    #[test]
717    fn z_flag_different_values_different_keys() {
718        let parsed1 = parse_rustc_args(&args(&["-Z", "query-threads=4", "src/lib.rs"]), &cwd());
719        let parsed2 = parse_rustc_args(&args(&["-Z", "query-threads=8", "src/lib.rs"]), &cwd());
720        assert_ne!(parsed1.unknown_flags, parsed2.unknown_flags);
721    }
722
723    #[test]
724    fn comma_separated_crate_types_split() {
725        let parsed = parse_rustc_args(&args(&["--crate-type", "lib,rlib", "src/lib.rs"]), &cwd());
726        assert_eq!(parsed.crate_types, vec!["lib", "rlib"]);
727    }
728
729    #[test]
730    fn relative_paths_resolved_against_cwd() {
731        let parsed = parse_rustc_args(&args(&["src/lib.rs"]), &cwd());
732        assert_eq!(
733            parsed.source_file,
734            NormalizedPath::from("/project/src/lib.rs")
735        );
736    }
737
738    #[test]
739    fn absolute_paths_unchanged() {
740        let parsed = parse_rustc_args(&args(&["/absolute/src/lib.rs"]), &cwd());
741        assert_eq!(
742            parsed.source_file,
743            NormalizedPath::from("/absolute/src/lib.rs")
744        );
745    }
746
747    #[test]
748    fn check_cfg_parsed() {
749        let parsed = parse_rustc_args(
750            &args(&["--check-cfg", "cfg(feature, values(\"std\"))", "src/lib.rs"]),
751            &cwd(),
752        );
753        assert_eq!(parsed.check_cfgs.len(), 1);
754    }
755
756    #[test]
757    fn sysroot_parsed() {
758        let parsed = parse_rustc_args(
759            &args(&[
760                "--sysroot",
761                "/home/user/.rustup/toolchains/stable",
762                "src/lib.rs",
763            ]),
764            &cwd(),
765        );
766        assert_eq!(
767            parsed.sysroot,
768            Some(NormalizedPath::from("/home/user/.rustup/toolchains/stable"))
769        );
770    }
771
772    #[test]
773    fn remap_path_prefix_parsed() {
774        let parsed = parse_rustc_args(
775            &args(&["--remap-path-prefix", "/home/user=/anon", "src/lib.rs"]),
776            &cwd(),
777        );
778        assert_eq!(parsed.remap_path_prefixes, vec!["/home/user=/anon"]);
779    }
780
781    #[test]
782    fn remap_path_prefix_equals_form_parsed() {
783        let parsed = parse_rustc_args(
784            &args(&["--remap-path-prefix=/home/user=/anon", "src/lib.rs"]),
785            &cwd(),
786        );
787        assert_eq!(parsed.remap_path_prefixes, vec!["/home/user=/anon"]);
788    }
789}