Skip to main content

ra_ap_test_utils/
fixture.rs

1//! Defines `Fixture` -- a convenient way to describe the initial state of
2//! rust-analyzer database from a single string.
3//!
4//! Fixtures are strings containing rust source code with optional metadata.
5//! A fixture without metadata is parsed into a single source file.
6//! Use this to test functionality local to one file.
7//!
8//! Simple Example:
9//!
10//! ```ignore
11//! r#"
12//! fn main() {
13//!     println!("Hello World")
14//! }
15//! "#
16//! ```
17//!
18//! Metadata can be added to a fixture after a `//-` comment.
19//! The basic form is specifying filenames,
20//! which is also how to define multiple files in a single test fixture
21//!
22//! Example using two files in the same crate:
23//!
24//! ```ignore
25//! "
26//! //- /main.rs
27//! mod foo;
28//! fn main() {
29//!     foo::bar();
30//! }
31//!
32//! //- /foo.rs
33//! pub fn bar() {}
34//! "
35//! ```
36//!
37//! Example using two crates with one file each, with one crate depending on the other:
38//!
39//! ```ignore
40//! r#"
41//! //- /main.rs crate:a deps:b
42//! fn main() {
43//!     b::foo();
44//! }
45//! //- /lib.rs crate:b
46//! pub fn b() {
47//!     println!("Hello World")
48//! }
49//! "#
50//! ```
51//!
52//! Metadata allows specifying all settings and variables
53//! that are available in a real rust project. See [`Fixture`]
54//! for the syntax.
55//!
56//! Example using some available metadata:
57//!
58//! ```ignore
59//! "
60//! //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
61//! fn insert_source_code_here() {}
62//! "
63//! ```
64
65use std::iter;
66
67use rustc_hash::FxHashMap;
68use stdx::trim_indent;
69
70#[derive(Debug, Eq, PartialEq)]
71pub struct Fixture {
72    /// Specifies the path for this file. It must start with "/".
73    pub path: String,
74    /// Defines a new crate and make this file its root module.
75    ///
76    /// Version and repository URL of the crate can optionally be specified; if
77    /// either one is specified, the other must also be specified.
78    ///
79    /// Syntax:
80    /// - `crate:my_awesome_lib`
81    /// - `crate:my_awesome_lib@0.0.1,https://example.com/repo.git`
82    pub krate: Option<String>,
83    /// Specifies dependencies of this crate. This must be used with `crate` meta.
84    ///
85    /// Syntax: `deps:hir-def,ide-assists`
86    pub deps: Vec<String>,
87    /// Limits crates in the extern prelude. The set of crate names must be a
88    /// subset of `deps`. This must be used with `crate` meta.
89    ///
90    /// If this is not specified, all the dependencies will be in the extern prelude.
91    ///
92    /// Syntax: `extern-prelude:hir-def,ide-assists`
93    pub extern_prelude: Option<Vec<String>>,
94    /// Specifies configuration options to be enabled. Options may have associated
95    /// values.
96    ///
97    /// Syntax: `cfg:test,dbg=false,opt_level=2`
98    pub cfgs: Vec<(String, Option<String>)>,
99    /// Specifies the edition of this crate. This must be used with
100    /// `crate` meta. If this is not specified,
101    /// `base_db::input::Edition::CURRENT` will be used.  This must be
102    /// used with `crate` meta.
103    ///
104    /// Syntax: `edition:2021`
105    pub edition: Option<String>,
106    /// Specifies environment variables.
107    ///
108    /// Syntax: `env:PATH=/bin,RUST_LOG=debug`
109    pub env: FxHashMap<String, String>,
110    /// Specifies extra crate-level attributes injected at the top of the crate root file.
111    /// This must be used with `crate` meta.
112    ///
113    /// Syntax: `crate-attr:no_std crate-attr:features(f16,f128) crate-attr:cfg(target_arch="x86")`
114    pub crate_attrs: Vec<String>,
115    /// Introduces a new source root. This file **and the following
116    /// files** will belong the new source root. This must be used
117    /// with `crate` meta.
118    ///
119    /// Use this if you want to test something that uses `SourceRoot::is_library()`
120    /// to check editability.
121    ///
122    /// Note that files before the first fixture with `new_source_root` meta will
123    /// belong to an implicitly defined local source root.
124    ///
125    /// Syntax:
126    /// - `new_source_root:library`
127    /// - `new_source_root:local`
128    pub introduce_new_source_root: Option<String>,
129    /// Explicitly declares this crate as a library outside current workspace. This
130    /// must be used with `crate` meta.
131    ///
132    /// This is implied if this file belongs to a library source root.
133    ///
134    /// Use this if you want to test something that checks if a crate is a workspace
135    /// member via `CrateOrigin`.
136    ///
137    /// Syntax: `library`
138    pub library: bool,
139    /// Actual file contents. All meta comments are stripped.
140    pub text: String,
141    /// The line number in the original fixture of the beginning of this fixture.
142    pub line: usize,
143}
144
145#[derive(Debug)]
146pub struct MiniCore {
147    activated_flags: Vec<String>,
148    valid_flags: Vec<String>,
149}
150
151#[derive(Debug)]
152pub struct FixtureWithProjectMeta {
153    pub fixture: Vec<Fixture>,
154    pub mini_core: Option<MiniCore>,
155    pub proc_macro_names: Vec<String>,
156    pub toolchain: Option<String>,
157    /// Specifies LLVM data layout to be used.
158    ///
159    /// You probably don't want to manually specify this. See LLVM manual for the
160    /// syntax, if you must: <https://llvm.org/docs/LangRef.html#data-layout>
161    pub target_data_layout: String,
162    /// Specifies the target architecture.
163    pub target_arch: String,
164}
165
166impl FixtureWithProjectMeta {
167    /// Parses text which looks like this:
168    ///
169    ///  ```text
170    ///  //- some meta
171    ///  line 1
172    ///  line 2
173    ///  //- other meta
174    ///  ```
175    ///
176    /// Fixture can also start with a proc_macros and minicore declaration (in that order):
177    ///
178    /// ```text
179    /// //- toolchain: nightly
180    /// //- proc_macros: identity
181    /// //- minicore: sized
182    /// ```
183    ///
184    /// That will set toolchain to nightly and include predefined proc macros and a subset of
185    /// `libcore` into the fixture, see `minicore.rs` for what's available. Note that toolchain
186    /// defaults to stable.
187    pub fn parse(#[rust_analyzer::rust_fixture] ra_fixture: &str) -> Self {
188        let fixture = trim_indent(ra_fixture);
189        let mut fixture = fixture.as_str();
190        let mut toolchain = None;
191        let mut target_data_layout =
192            "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128".to_owned();
193        let mut target_arch = "x86_64".to_owned();
194        let mut mini_core = None;
195        let mut res: Vec<Fixture> = Vec::new();
196        let mut proc_macro_names = vec![];
197        let mut first_row = 0;
198
199        if let Some(meta) = fixture.strip_prefix("//- toolchain:") {
200            first_row += 1;
201            let (meta, remain) = meta.split_once('\n').unwrap();
202            toolchain = Some(meta.trim().to_owned());
203            fixture = remain;
204        }
205
206        if let Some(meta) = fixture.strip_prefix("//- target_data_layout:") {
207            first_row += 1;
208            let (meta, remain) = meta.split_once('\n').unwrap();
209            meta.trim().clone_into(&mut target_data_layout);
210            fixture = remain;
211        }
212
213        if let Some(meta) = fixture.strip_prefix("//- target_arch:") {
214            first_row += 1;
215            let (meta, remain) = meta.split_once('\n').unwrap();
216            meta.trim().clone_into(&mut target_arch);
217            fixture = remain;
218        }
219
220        if let Some(meta) = fixture.strip_prefix("//- proc_macros:") {
221            first_row += 1;
222            let (meta, remain) = meta.split_once('\n').unwrap();
223            proc_macro_names = meta.split(',').map(|it| it.trim().to_owned()).collect();
224            fixture = remain;
225        }
226
227        if let Some(meta) = fixture.strip_prefix("//- minicore:") {
228            first_row += 1;
229            let (meta, remain) = meta.split_once('\n').unwrap();
230            mini_core = Some(MiniCore::parse(meta));
231            fixture = remain;
232        }
233
234        let default =
235            if fixture.contains("//- /") { None } else { Some((first_row - 1, "//- /main.rs")) };
236
237        for (ix, line) in
238            default.into_iter().chain((first_row..).zip(fixture.split_inclusive('\n')))
239        {
240            if line.contains("//-") {
241                assert!(
242                    line.starts_with("//-"),
243                    "Metadata line {ix} has invalid indentation. \
244                     All metadata lines need to have the same indentation.\n\
245                     The offending line: {line:?}"
246                );
247            }
248
249            if let Some(line) = line.strip_prefix("//-") {
250                let meta = Self::parse_meta_line(line, (ix + 1).try_into().unwrap());
251                res.push(meta);
252            } else {
253                if matches!(line.strip_prefix("// "), Some(l) if l.trim().starts_with('/')) {
254                    panic!("looks like invalid metadata line: {line:?}");
255                }
256
257                if let Some(entry) = res.last_mut() {
258                    entry.text.push_str(line);
259                }
260            }
261        }
262
263        Self {
264            fixture: res,
265            mini_core,
266            proc_macro_names,
267            toolchain,
268            target_data_layout,
269            target_arch,
270        }
271    }
272
273    //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
274    fn parse_meta_line(meta: &str, line: usize) -> Fixture {
275        let meta = meta.trim();
276        let mut components = meta.split_ascii_whitespace();
277
278        let path = components.next().expect("fixture meta must start with a path").to_owned();
279        assert!(path.starts_with('/'), "fixture path does not start with `/`: {path:?}");
280
281        let mut krate = None;
282        let mut deps = Vec::new();
283        let mut crate_attrs = Vec::new();
284        let mut extern_prelude = None;
285        let mut edition = None;
286        let mut cfgs = Vec::new();
287        let mut env = FxHashMap::default();
288        let mut introduce_new_source_root = None;
289        let mut library = false;
290        for component in components {
291            if component == "library" {
292                library = true;
293                continue;
294            }
295
296            let (key, value) =
297                component.split_once(':').unwrap_or_else(|| panic!("invalid meta line: {meta:?}"));
298            match key {
299                "crate" => krate = Some(value.to_owned()),
300                "deps" => deps = value.split(',').map(|it| it.to_owned()).collect(),
301                "crate-attr" => crate_attrs.push(value.to_owned()),
302                "extern-prelude" => {
303                    if value.is_empty() {
304                        extern_prelude = Some(Vec::new());
305                    } else {
306                        extern_prelude =
307                            Some(value.split(',').map(|it| it.to_owned()).collect::<Vec<_>>());
308                    }
309                }
310                "edition" => edition = Some(value.to_owned()),
311                "cfg" => {
312                    for entry in value.split(',') {
313                        match entry.split_once('=') {
314                            Some((k, v)) => cfgs.push((k.to_owned(), Some(v.to_owned()))),
315                            None => cfgs.push((entry.to_owned(), None)),
316                        }
317                    }
318                }
319                "env" => {
320                    for key in value.split(',') {
321                        if let Some((k, v)) = key.split_once('=') {
322                            env.insert(k.into(), v.into());
323                        }
324                    }
325                }
326                "new_source_root" => introduce_new_source_root = Some(value.to_owned()),
327                _ => panic!("bad component: {component:?}"),
328            }
329        }
330
331        for prelude_dep in extern_prelude.iter().flatten() {
332            assert!(
333                deps.contains(prelude_dep),
334                "extern-prelude {extern_prelude:?} must be a subset of deps {deps:?}"
335            );
336        }
337
338        Fixture {
339            path,
340            text: String::new(),
341            line,
342            krate,
343            deps,
344            crate_attrs,
345            extern_prelude,
346            cfgs,
347            edition,
348            env,
349            introduce_new_source_root,
350            library,
351        }
352    }
353}
354
355impl MiniCore {
356    pub const RAW_SOURCE: &'static str = include_str!("./minicore.rs");
357
358    fn has_flag(&self, flag: &str) -> bool {
359        self.activated_flags.iter().any(|it| it == flag)
360    }
361
362    pub fn from_flags<'a>(flags: impl IntoIterator<Item = &'a str>) -> Self {
363        MiniCore {
364            activated_flags: flags.into_iter().map(|x| x.to_owned()).collect(),
365            valid_flags: Vec::new(),
366        }
367    }
368
369    #[track_caller]
370    fn assert_valid_flag(&self, flag: &str) {
371        if !self.valid_flags.iter().any(|it| it == flag) {
372            panic!("invalid flag: {flag:?}, valid flags: {:?}", self.valid_flags);
373        }
374    }
375
376    fn parse(line: &str) -> MiniCore {
377        let mut res = MiniCore { activated_flags: Vec::new(), valid_flags: Vec::new() };
378
379        for entry in line.trim().split(", ") {
380            if res.has_flag(entry) {
381                panic!("duplicate minicore flag: {entry:?}");
382            }
383            res.activated_flags.push(entry.to_owned());
384        }
385
386        res
387    }
388
389    pub fn available_flags(raw_source: &str) -> impl Iterator<Item = &str> {
390        let lines = raw_source.split_inclusive('\n');
391        lines
392            .map_while(|x| x.strip_prefix("//!"))
393            .skip_while(|line| !line.contains("Available flags:"))
394            .skip(1)
395            .map(|x| x.split_once(':').unwrap().0.trim())
396    }
397
398    /// Strips parts of minicore.rs which are flagged by inactive flags.
399    ///
400    /// This is probably over-engineered to support flags dependencies.
401    pub fn source_code(mut self, raw_source: &str) -> String {
402        let mut buf = String::new();
403        let mut lines = raw_source.split_inclusive('\n');
404
405        let mut implications = Vec::new();
406
407        // Parse `//!` preamble and extract flags and dependencies.
408        let trim_doc: fn(&str) -> Option<&str> = |line| match line.strip_prefix("//!") {
409            Some(it) => Some(it),
410            None => {
411                assert!(line.trim().is_empty(), "expected empty line after minicore header");
412                None
413            }
414        };
415        for line in lines
416            .by_ref()
417            .map_while(trim_doc)
418            .skip_while(|line| !line.contains("Available flags:"))
419            .skip(1)
420        {
421            let (flag, deps) = line.split_once(':').unwrap();
422            let flag = flag.trim();
423
424            self.valid_flags.push(flag.to_owned());
425            implications.extend(
426                iter::repeat(flag)
427                    .zip(deps.split(", ").map(str::trim).filter(|dep| !dep.is_empty())),
428            );
429        }
430
431        for (_, dep) in &implications {
432            self.assert_valid_flag(dep);
433        }
434
435        for flag in &self.activated_flags {
436            self.assert_valid_flag(flag);
437        }
438
439        // Fixed point loop to compute transitive closure of flags.
440        loop {
441            let mut changed = false;
442            for &(u, v) in &implications {
443                if self.has_flag(u) && !self.has_flag(v) {
444                    self.activated_flags.push(v.to_owned());
445                    changed = true;
446                }
447            }
448            if !changed {
449                break;
450            }
451        }
452
453        let mut active_regions = Vec::new();
454        let mut inactive_regions = Vec::new();
455        let mut seen_regions = Vec::new();
456        for line in lines {
457            let trimmed = line.trim();
458            if let Some(region) = trimmed.strip_prefix("// region:") {
459                if let Some(region) = region.strip_prefix('!') {
460                    inactive_regions.push(region);
461                    continue;
462                } else {
463                    active_regions.push(region);
464                    continue;
465                }
466            }
467            if let Some(region) = trimmed.strip_prefix("// endregion:") {
468                let (prev, region) = if let Some(region) = region.strip_prefix('!') {
469                    (inactive_regions.pop().unwrap(), region)
470                } else {
471                    (active_regions.pop().unwrap(), region)
472                };
473                assert_eq!(prev, region, "unbalanced region pairs");
474                continue;
475            }
476
477            let mut active_line_region = 0;
478            let mut inactive_line_region = 0;
479            if let Some(idx) = trimmed.find("// :!") {
480                let regions = trimmed[idx + "// :!".len()..].split(", ");
481                inactive_line_region += regions.clone().count();
482                inactive_regions.extend(regions);
483            } else if let Some(idx) = trimmed.find("// :") {
484                let regions = trimmed[idx + "// :".len()..].split(", ");
485                active_line_region += regions.clone().count();
486                active_regions.extend(regions);
487            }
488
489            let mut keep = true;
490            for &region in &active_regions {
491                assert!(!region.starts_with(' '), "region marker starts with a space: {region:?}");
492                self.assert_valid_flag(region);
493                seen_regions.push(region);
494                keep &= self.has_flag(region);
495            }
496            for &region in &inactive_regions {
497                assert!(!region.starts_with(' '), "region marker starts with a space: {region:?}");
498                self.assert_valid_flag(region);
499                seen_regions.push(region);
500                keep &= !self.has_flag(region);
501            }
502
503            if keep {
504                buf.push_str(line);
505            }
506            if active_line_region > 0 {
507                active_regions.drain(active_regions.len() - active_line_region..);
508            }
509            if inactive_line_region > 0 {
510                inactive_regions.drain(inactive_regions.len() - active_line_region..);
511            }
512        }
513
514        if !active_regions.is_empty() {
515            panic!("unclosed regions: {active_regions:?} Add an `endregion` comment");
516        }
517        if !inactive_regions.is_empty() {
518            panic!("unclosed regions: {inactive_regions:?} Add an `endregion` comment");
519        }
520
521        for flag in &self.valid_flags {
522            if !seen_regions.iter().any(|it| it == flag) {
523                panic!("unused minicore flag: {flag:?}");
524            }
525        }
526        buf
527    }
528}
529
530#[test]
531#[should_panic]
532fn parse_fixture_checks_further_indented_metadata() {
533    FixtureWithProjectMeta::parse(
534        r"
535        //- /lib.rs
536          mod bar;
537
538          fn foo() {}
539          //- /bar.rs
540          pub fn baz() {}
541          ",
542    );
543}
544
545#[test]
546fn parse_fixture_gets_full_meta() {
547    let FixtureWithProjectMeta {
548        fixture: parsed,
549        mini_core,
550        proc_macro_names,
551        toolchain,
552        target_data_layout: _,
553        target_arch: _,
554    } = FixtureWithProjectMeta::parse(
555        r#"
556//- toolchain: nightly
557//- proc_macros: identity
558//- minicore: coerce_unsized
559//- /lib.rs crate:foo deps:bar,baz crate-attr:no_std crate-attr:features(f16,f128) crate-attr:cfg(target_arch="x86") cfg:foo=a,bar=b,atom env:OUTDIR=path/to,OTHER=foo
560mod m;
561"#,
562    );
563    assert_eq!(toolchain, Some("nightly".to_owned()));
564    assert_eq!(proc_macro_names, vec!["identity".to_owned()]);
565    assert_eq!(mini_core.unwrap().activated_flags, vec!["coerce_unsized".to_owned()]);
566    assert_eq!(1, parsed.len());
567
568    let meta = &parsed[0];
569    assert_eq!("mod m;\n", meta.text);
570
571    assert_eq!("foo", meta.krate.as_ref().unwrap());
572    assert_eq!(
573        vec![
574            "no_std".to_owned(),
575            "features(f16,f128)".to_owned(),
576            "cfg(target_arch=\"x86\")".to_owned()
577        ],
578        meta.crate_attrs
579    );
580    assert_eq!("/lib.rs", meta.path);
581    assert_eq!(2, meta.env.len());
582}