1use std::iter;
66
67use rustc_hash::FxHashMap;
68use stdx::trim_indent;
69
70#[derive(Debug, Eq, PartialEq)]
71pub struct Fixture {
72 pub path: String,
74 pub krate: Option<String>,
83 pub deps: Vec<String>,
87 pub extern_prelude: Option<Vec<String>>,
94 pub cfgs: Vec<(String, Option<String>)>,
99 pub edition: Option<String>,
106 pub env: FxHashMap<String, String>,
110 pub crate_attrs: Vec<String>,
115 pub introduce_new_source_root: Option<String>,
129 pub library: bool,
139 pub text: String,
141 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 pub target_data_layout: String,
162 pub target_arch: String,
164}
165
166impl FixtureWithProjectMeta {
167 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 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 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 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 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 ®ion 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 ®ion 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}