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 `crate` meta. If
100    /// this is not specified, ([`base_db::input::Edition::CURRENT`]) will be used.
101    /// This must be used with `crate` meta.
102    ///
103    /// Syntax: `edition:2021`
104    pub edition: Option<String>,
105    /// Specifies environment variables.
106    ///
107    /// Syntax: `env:PATH=/bin,RUST_LOG=debug`
108    pub env: FxHashMap<String, String>,
109    /// Introduces a new [source root](base_db::input::SourceRoot). This file **and
110    /// the following files** will belong the new source root. This must be used
111    /// with `crate` meta.
112    ///
113    /// Use this if you want to test something that uses `SourceRoot::is_library()`
114    /// to check editability.
115    ///
116    /// Note that files before the first fixture with `new_source_root` meta will
117    /// belong to an implicitly defined local source root.
118    ///
119    /// Syntax:
120    /// - `new_source_root:library`
121    /// - `new_source_root:local`
122    pub introduce_new_source_root: Option<String>,
123    /// Explicitly declares this crate as a library outside current workspace. This
124    /// must be used with `crate` meta.
125    ///
126    /// This is implied if this file belongs to a library source root.
127    ///
128    /// Use this if you want to test something that checks if a crate is a workspace
129    /// member via [`CrateOrigin`](base_db::input::CrateOrigin).
130    ///
131    /// Syntax: `library`
132    pub library: bool,
133    /// Actual file contents. All meta comments are stripped.
134    pub text: String,
135}
136
137pub struct MiniCore {
138    activated_flags: Vec<String>,
139    valid_flags: Vec<String>,
140}
141
142pub struct FixtureWithProjectMeta {
143    pub fixture: Vec<Fixture>,
144    pub mini_core: Option<MiniCore>,
145    pub proc_macro_names: Vec<String>,
146    pub toolchain: Option<String>,
147    /// Specifies LLVM data layout to be used.
148    ///
149    /// You probably don't want to manually specify this. See LLVM manual for the
150    /// syntax, if you must: <https://llvm.org/docs/LangRef.html#data-layout>
151    pub target_data_layout: String,
152}
153
154impl FixtureWithProjectMeta {
155    /// Parses text which looks like this:
156    ///
157    ///  ```text
158    ///  //- some meta
159    ///  line 1
160    ///  line 2
161    ///  //- other meta
162    ///  ```
163    ///
164    /// Fixture can also start with a proc_macros and minicore declaration (in that order):
165    ///
166    /// ```text
167    /// //- toolchain: nightly
168    /// //- proc_macros: identity
169    /// //- minicore: sized
170    /// ```
171    ///
172    /// That will set toolchain to nightly and include predefined proc macros and a subset of
173    /// `libcore` into the fixture, see `minicore.rs` for what's available. Note that toolchain
174    /// defaults to stable.
175    pub fn parse(#[rust_analyzer::rust_fixture] ra_fixture: &str) -> Self {
176        let fixture = trim_indent(ra_fixture);
177        let mut fixture = fixture.as_str();
178        let mut toolchain = None;
179        let mut target_data_layout =
180            "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128".to_owned();
181        let mut mini_core = None;
182        let mut res: Vec<Fixture> = Vec::new();
183        let mut proc_macro_names = vec![];
184
185        if let Some(meta) = fixture.strip_prefix("//- toolchain:") {
186            let (meta, remain) = meta.split_once('\n').unwrap();
187            toolchain = Some(meta.trim().to_owned());
188            fixture = remain;
189        }
190
191        if let Some(meta) = fixture.strip_prefix("//- target_data_layout:") {
192            let (meta, remain) = meta.split_once('\n').unwrap();
193            meta.trim().clone_into(&mut target_data_layout);
194            fixture = remain;
195        }
196
197        if let Some(meta) = fixture.strip_prefix("//- proc_macros:") {
198            let (meta, remain) = meta.split_once('\n').unwrap();
199            proc_macro_names = meta.split(',').map(|it| it.trim().to_owned()).collect();
200            fixture = remain;
201        }
202
203        if let Some(meta) = fixture.strip_prefix("//- minicore:") {
204            let (meta, remain) = meta.split_once('\n').unwrap();
205            mini_core = Some(MiniCore::parse(meta));
206            fixture = remain;
207        }
208
209        let default = if fixture.contains("//-") { None } else { Some("//- /main.rs") };
210
211        for (ix, line) in default.into_iter().chain(fixture.split_inclusive('\n')).enumerate() {
212            if line.contains("//-") {
213                assert!(
214                    line.starts_with("//-"),
215                    "Metadata line {ix} has invalid indentation. \
216                     All metadata lines need to have the same indentation.\n\
217                     The offending line: {line:?}"
218                );
219            }
220
221            if let Some(line) = line.strip_prefix("//-") {
222                let meta = Self::parse_meta_line(line);
223                res.push(meta);
224            } else {
225                if matches!(line.strip_prefix("// "), Some(l) if l.trim().starts_with('/')) {
226                    panic!("looks like invalid metadata line: {line:?}");
227                }
228
229                if let Some(entry) = res.last_mut() {
230                    entry.text.push_str(line);
231                }
232            }
233        }
234
235        Self { fixture: res, mini_core, proc_macro_names, toolchain, target_data_layout }
236    }
237
238    //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
239    fn parse_meta_line(meta: &str) -> Fixture {
240        let meta = meta.trim();
241        let mut components = meta.split_ascii_whitespace();
242
243        let path = components.next().expect("fixture meta must start with a path").to_owned();
244        assert!(path.starts_with('/'), "fixture path does not start with `/`: {path:?}");
245
246        let mut krate = None;
247        let mut deps = Vec::new();
248        let mut extern_prelude = None;
249        let mut edition = None;
250        let mut cfgs = Vec::new();
251        let mut env = FxHashMap::default();
252        let mut introduce_new_source_root = None;
253        let mut library = false;
254        for component in components {
255            if component == "library" {
256                library = true;
257                continue;
258            }
259
260            let (key, value) =
261                component.split_once(':').unwrap_or_else(|| panic!("invalid meta line: {meta:?}"));
262            match key {
263                "crate" => krate = Some(value.to_owned()),
264                "deps" => deps = value.split(',').map(|it| it.to_owned()).collect(),
265                "extern-prelude" => {
266                    if value.is_empty() {
267                        extern_prelude = Some(Vec::new());
268                    } else {
269                        extern_prelude =
270                            Some(value.split(',').map(|it| it.to_owned()).collect::<Vec<_>>());
271                    }
272                }
273                "edition" => edition = Some(value.to_owned()),
274                "cfg" => {
275                    for entry in value.split(',') {
276                        match entry.split_once('=') {
277                            Some((k, v)) => cfgs.push((k.to_owned(), Some(v.to_owned()))),
278                            None => cfgs.push((entry.to_owned(), None)),
279                        }
280                    }
281                }
282                "env" => {
283                    for key in value.split(',') {
284                        if let Some((k, v)) = key.split_once('=') {
285                            env.insert(k.into(), v.into());
286                        }
287                    }
288                }
289                "new_source_root" => introduce_new_source_root = Some(value.to_owned()),
290                _ => panic!("bad component: {component:?}"),
291            }
292        }
293
294        for prelude_dep in extern_prelude.iter().flatten() {
295            assert!(
296                deps.contains(prelude_dep),
297                "extern-prelude {extern_prelude:?} must be a subset of deps {deps:?}"
298            );
299        }
300
301        Fixture {
302            path,
303            text: String::new(),
304            krate,
305            deps,
306            extern_prelude,
307            cfgs,
308            edition,
309            env,
310            introduce_new_source_root,
311            library,
312        }
313    }
314}
315
316impl MiniCore {
317    const RAW_SOURCE: &'static str = include_str!("./minicore.rs");
318
319    fn has_flag(&self, flag: &str) -> bool {
320        self.activated_flags.iter().any(|it| it == flag)
321    }
322
323    pub fn from_flags<'a>(flags: impl IntoIterator<Item = &'a str>) -> Self {
324        MiniCore {
325            activated_flags: flags.into_iter().map(|x| x.to_owned()).collect(),
326            valid_flags: Vec::new(),
327        }
328    }
329
330    #[track_caller]
331    fn assert_valid_flag(&self, flag: &str) {
332        if !self.valid_flags.iter().any(|it| it == flag) {
333            panic!("invalid flag: {flag:?}, valid flags: {:?}", self.valid_flags);
334        }
335    }
336
337    fn parse(line: &str) -> MiniCore {
338        let mut res = MiniCore { activated_flags: Vec::new(), valid_flags: Vec::new() };
339
340        for entry in line.trim().split(", ") {
341            if res.has_flag(entry) {
342                panic!("duplicate minicore flag: {entry:?}");
343            }
344            res.activated_flags.push(entry.to_owned());
345        }
346
347        res
348    }
349
350    pub fn available_flags() -> impl Iterator<Item = &'static str> {
351        let lines = MiniCore::RAW_SOURCE.split_inclusive('\n');
352        lines
353            .map_while(|x| x.strip_prefix("//!"))
354            .skip_while(|line| !line.contains("Available flags:"))
355            .skip(1)
356            .map(|x| x.split_once(':').unwrap().0.trim())
357    }
358
359    /// Strips parts of minicore.rs which are flagged by inactive flags.
360    ///
361    /// This is probably over-engineered to support flags dependencies.
362    pub fn source_code(mut self) -> String {
363        let mut buf = String::new();
364        let mut lines = MiniCore::RAW_SOURCE.split_inclusive('\n');
365
366        let mut implications = Vec::new();
367
368        // Parse `//!` preamble and extract flags and dependencies.
369        let trim_doc: fn(&str) -> Option<&str> = |line| match line.strip_prefix("//!") {
370            Some(it) => Some(it),
371            None => {
372                assert!(line.trim().is_empty(), "expected empty line after minicore header");
373                None
374            }
375        };
376        for line in lines
377            .by_ref()
378            .map_while(trim_doc)
379            .skip_while(|line| !line.contains("Available flags:"))
380            .skip(1)
381        {
382            let (flag, deps) = line.split_once(':').unwrap();
383            let flag = flag.trim();
384
385            self.valid_flags.push(flag.to_owned());
386            implications.extend(
387                iter::repeat(flag)
388                    .zip(deps.split(", ").map(str::trim).filter(|dep| !dep.is_empty())),
389            );
390        }
391
392        for (_, dep) in &implications {
393            self.assert_valid_flag(dep);
394        }
395
396        for flag in &self.activated_flags {
397            self.assert_valid_flag(flag);
398        }
399
400        // Fixed point loop to compute transitive closure of flags.
401        loop {
402            let mut changed = false;
403            for &(u, v) in &implications {
404                if self.has_flag(u) && !self.has_flag(v) {
405                    self.activated_flags.push(v.to_owned());
406                    changed = true;
407                }
408            }
409            if !changed {
410                break;
411            }
412        }
413
414        let mut active_regions = Vec::new();
415        let mut seen_regions = Vec::new();
416        for line in lines {
417            let trimmed = line.trim();
418            if let Some(region) = trimmed.strip_prefix("// region:") {
419                active_regions.push(region);
420                continue;
421            }
422            if let Some(region) = trimmed.strip_prefix("// endregion:") {
423                let prev = active_regions.pop().unwrap();
424                assert_eq!(prev, region, "unbalanced region pairs");
425                continue;
426            }
427
428            let mut line_region = false;
429            if let Some(idx) = trimmed.find("// :") {
430                line_region = true;
431                active_regions.push(&trimmed[idx + "// :".len()..]);
432            }
433
434            let mut keep = true;
435            for &region in &active_regions {
436                assert!(!region.starts_with(' '), "region marker starts with a space: {region:?}");
437                self.assert_valid_flag(region);
438                seen_regions.push(region);
439                keep &= self.has_flag(region);
440            }
441
442            if keep {
443                buf.push_str(line);
444            }
445            if line_region {
446                active_regions.pop().unwrap();
447            }
448        }
449
450        if !active_regions.is_empty() {
451            panic!("unclosed regions: {active_regions:?} Add an `endregion` comment");
452        }
453
454        for flag in &self.valid_flags {
455            if !seen_regions.iter().any(|it| it == flag) {
456                panic!("unused minicore flag: {flag:?}");
457            }
458        }
459        buf
460    }
461}
462
463#[test]
464#[should_panic]
465fn parse_fixture_checks_further_indented_metadata() {
466    FixtureWithProjectMeta::parse(
467        r"
468        //- /lib.rs
469          mod bar;
470
471          fn foo() {}
472          //- /bar.rs
473          pub fn baz() {}
474          ",
475    );
476}
477
478#[test]
479fn parse_fixture_gets_full_meta() {
480    let FixtureWithProjectMeta {
481        fixture: parsed,
482        mini_core,
483        proc_macro_names,
484        toolchain,
485        target_data_layout: _,
486    } = FixtureWithProjectMeta::parse(
487        r#"
488//- toolchain: nightly
489//- proc_macros: identity
490//- minicore: coerce_unsized
491//- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b,atom env:OUTDIR=path/to,OTHER=foo
492mod m;
493"#,
494    );
495    assert_eq!(toolchain, Some("nightly".to_owned()));
496    assert_eq!(proc_macro_names, vec!["identity".to_owned()]);
497    assert_eq!(mini_core.unwrap().activated_flags, vec!["coerce_unsized".to_owned()]);
498    assert_eq!(1, parsed.len());
499
500    let meta = &parsed[0];
501    assert_eq!("mod m;\n", meta.text);
502
503    assert_eq!("foo", meta.krate.as_ref().unwrap());
504    assert_eq!("/lib.rs", meta.path);
505    assert_eq!(2, meta.env.len());
506}