pub mod syntax;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::sync::atomic::AtomicBool;
use crate::rustc_feature::UnstableFeatures;
use crate::rustc_hir::def::DefKind;
use crate::rustc_hir::def_id::{LOCAL_CRATE, LocalDefId};
use crate::rustc_hir::intravisit::{self, Visitor};
use crate::rustc_hir::{self as hir, ExprKind, ItemKind, Node, UseKind};
#[cfg(not(feature = "force_pinned_sysroot"))]
use crate::rustc_interface::util::rustc_version_of_sysroot;
use crate::rustc_interface::{Config, create_and_enter_global_ctxt, parse, run_compiler};
use crate::rustc_middle::ty::{TyCtxt, TypeVisitableExt};
use crate::rustc_session::config::{Input, Options, Sysroot};
use crate::rustc_span::fatal_error::{FatalError, catch_fatal_errors};
use crate::rustc_span::{FileName, Span};
use crate::rustc_structures::CrateType;
use serde::{Deserialize, Serialize};
pub mod site;
#[cfg(feature = "diagnostics")]
pub mod diagnostics;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ByteSpan {
pub file: String,
pub start: u32,
pub end: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FactKind {
Fn,
AssocFn,
Struct,
Enum,
Union,
Variant,
Trait,
TraitAlias,
Mod,
Const,
Static,
TyAlias,
AssocTy,
AssocConst,
Macro,
Field,
Closure,
Other,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Definition {
pub def_path: String,
pub name: String,
pub kind: FactKind,
pub span: ByteSpan,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<FnSignature>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<FieldSignature>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub variants: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FnSignature {
pub receiver: Option<Receiver>,
pub params: Vec<FnParam>,
pub ret: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FnParam {
pub pat: String,
pub ty: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReceiverKind {
Value,
Ref,
RefMut,
Typed,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Receiver {
pub kind: ReceiverKind,
pub ty: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FieldSignature {
pub name: String,
pub ty: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Import {
pub module_def_path: String,
pub path: String,
pub span: ByteSpan,
pub reexport: bool,
pub bindings: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Impl {
pub def_path: String,
pub self_type: String,
pub trait_def_path: Option<String>,
pub span: ByteSpan,
#[serde(default)]
pub items: Vec<ImplItem>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ImplItem {
pub name: String,
pub kind: FactKind,
pub def_path: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RefKind {
Path,
Method,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Reference {
pub from_def_path: String,
pub to_def_path: String,
pub kind: RefKind,
pub span: ByteSpan,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CrateFacts {
pub crate_name: String,
pub definitions: Vec<Definition>,
pub imports: Vec<Import>,
pub impls: Vec<Impl>,
pub references: Vec<Reference>,
#[serde(default)]
pub diagnostics: Vec<String>,
#[serde(default)]
pub unanalyzed_bodies: Vec<String>,
#[serde(default = "complete_when_absent")]
pub complete: bool,
}
fn complete_when_absent() -> bool {
true
}
pub fn fact_kind(kind: DefKind) -> Option<FactKind> {
Some(match kind {
DefKind::Fn => FactKind::Fn,
DefKind::AssocFn => FactKind::AssocFn,
DefKind::Struct => FactKind::Struct,
DefKind::Enum => FactKind::Enum,
DefKind::Union => FactKind::Union,
DefKind::Variant => FactKind::Variant,
DefKind::Trait => FactKind::Trait,
DefKind::TraitAlias => FactKind::TraitAlias,
DefKind::Mod => FactKind::Mod,
DefKind::Const { .. } => FactKind::Const,
DefKind::Static { .. } => FactKind::Static,
DefKind::TyAlias => FactKind::TyAlias,
DefKind::AssocTy => FactKind::AssocTy,
DefKind::AssocConst { .. } => FactKind::AssocConst,
DefKind::Macro(_) => FactKind::Macro,
DefKind::Field => FactKind::Field,
DefKind::Closure => FactKind::Closure,
DefKind::ForeignTy => FactKind::Other,
DefKind::Use
| DefKind::Impl { .. }
| DefKind::ExternCrate
| DefKind::ForeignMod
| DefKind::AnonConst
| DefKind::OpaqueTy
| DefKind::TyParam
| DefKind::ConstParam
| DefKind::LifetimeParam
| DefKind::GlobalAsm
| DefKind::Ctor(..)
| DefKind::SyntheticCoroutineBody
| DefKind::TestBinderConstraints => return None,
})
}
pub fn extract(tcx: TyCtxt<'_>) -> CrateFacts {
let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
let mut facts = CrateFacts { crate_name, ..CrateFacts::default() };
let _ = tcx.hir_crate_items(());
let count = tcx.untracked().definitions.read().num_definitions();
for local in (0..count).map(|i| LocalDefId {
local_def_index: crate::rustc_span::def_id::DefIndex::from_usize(i),
}) {
let def_id = local.to_def_id();
let kind = tcx.def_kind(def_id);
if let DefKind::Impl { .. } = kind {
facts.impls.push(impl_fact(tcx, local));
continue;
}
let Some(kind) = fact_kind(kind) else {
continue;
};
let Some(name) = tcx.opt_item_name(def_id) else {
continue;
};
let mut definition = Definition {
def_path: tcx.def_path_str(def_id),
name: name.to_string(),
kind,
span: byte_span(tcx, tcx.def_span(def_id)),
signature: None,
fields: Vec::new(),
variants: Vec::new(),
};
describe_shape(tcx, local, &mut definition);
facts.definitions.push(definition);
}
for item_id in tcx.hir_free_items() {
let item = tcx.hir_item(item_id);
let ItemKind::Use(path, use_kind) = item.kind else {
continue;
};
let path_str = path
.segments
.iter()
.map(|seg| seg.ident.as_str())
.filter(|s| *s != "{{root}}")
.collect::<Vec<_>>()
.join("::");
let (path_str, bindings) = match use_kind {
UseKind::Glob => (format!("{path_str}::*"), Vec::new()),
UseKind::Single(ident) => (path_str, vec![ident.as_str().to_string()]),
UseKind::ListStem => continue,
};
let module = tcx.parent_module_from_def_id(item.owner_id.def_id);
facts.imports.push(Import {
module_def_path: tcx.def_path_str(module.to_def_id()),
path: path_str,
span: byte_span(tcx, item.span),
reexport: tcx.local_visibility(item.owner_id.def_id).is_public(),
bindings,
});
}
for owner in tcx.hir_body_owners() {
let Some(body) = tcx.hir_maybe_body_owned_by(owner) else {
continue;
};
let from = tcx.def_path_str(owner.to_def_id());
let Ok(typeck) = catch_fatal_errors(|| tcx.typeck(owner)) else {
facts.unanalyzed_bodies.push(from);
continue;
};
let mut visitor = RefVisitor { tcx, typeck, from: &from, refs: &mut facts.references };
visitor.visit_expr(body.value);
}
facts.complete = tcx.dcx().has_errors().is_none() && facts.unanalyzed_bodies.is_empty();
facts
.definitions
.sort_by(|a, b| a.span.start.cmp(&b.span.start).then(a.def_path.cmp(&b.def_path)));
facts.imports.sort_by(|a, b| a.span.start.cmp(&b.span.start).then(a.path.cmp(&b.path)));
facts.impls.sort_by(|a, b| a.span.start.cmp(&b.span.start).then(a.def_path.cmp(&b.def_path)));
facts
.references
.sort_by(|a, b| a.span.start.cmp(&b.span.start).then(a.to_def_path.cmp(&b.to_def_path)));
facts
}
fn impl_fact(tcx: TyCtxt<'_>, local: LocalDefId) -> Impl {
let def_id = local.to_def_id();
let hir_impl = match tcx.hir_node_by_def_id(local) {
Node::Item(hir::Item { kind: ItemKind::Impl(hir_impl), .. }) => Some(hir_impl),
_ => None,
};
let self_type = catch_fatal_errors(|| {
let ty = tcx.type_of(def_id).instantiate_identity().skip_normalization();
(!ty.references_error()).then(|| ty.to_string())
})
.ok()
.flatten()
.or_else(|| hir_impl.map(|hir_impl| type_text(tcx, hir_impl.self_ty)))
.unwrap_or_default();
let trait_def_path = catch_fatal_errors(|| tcx.impl_opt_trait_id(def_id))
.ok()
.flatten()
.map(|trait_id| tcx.def_path_str(trait_id));
let items = hir_impl
.map(|hir_impl| {
hir_impl
.items
.iter()
.filter_map(|item| {
let item_id = item.owner_id.to_def_id();
Some(ImplItem {
name: tcx.opt_item_name(item_id)?.to_string(),
kind: fact_kind(tcx.def_kind(item_id))?,
def_path: tcx.def_path_str(item_id),
})
})
.collect()
})
.unwrap_or_default();
Impl {
def_path: tcx.def_path_str(def_id),
self_type,
trait_def_path,
span: byte_span(tcx, tcx.def_span(def_id)),
items,
}
}
fn describe_shape(tcx: TyCtxt<'_>, local: LocalDefId, definition: &mut Definition) {
match definition.kind {
FactKind::Fn | FactKind::AssocFn => {
definition.signature = fn_signature(tcx, tcx.hir_node_by_def_id(local));
}
FactKind::Struct | FactKind::Union => {
if let Node::Item(hir::Item {
kind: ItemKind::Struct(_, _, data) | ItemKind::Union(_, _, data),
..
}) = tcx.hir_node_by_def_id(local)
{
definition.fields = data
.fields()
.iter()
.map(|field| FieldSignature {
name: field.ident.as_str().to_string(),
ty: type_text(tcx, field.ty),
})
.collect();
}
}
FactKind::Enum => {
if let Node::Item(hir::Item { kind: ItemKind::Enum(_, _, def), .. }) =
tcx.hir_node_by_def_id(local)
{
definition.variants =
def.variants.iter().map(|variant| variant.ident.as_str().to_string()).collect();
}
}
_ => {}
}
}
fn fn_signature<'tcx>(tcx: TyCtxt<'tcx>, node: Node<'tcx>) -> Option<FnSignature> {
let sig = node.fn_sig()?;
let (body, names): (Option<&hir::Body<'_>>, &[Option<crate::rustc_span::Ident>]) = match node {
Node::Item(hir::Item { kind: ItemKind::Fn { body, .. }, .. })
| Node::ImplItem(hir::ImplItem { kind: hir::ImplItemKind::Fn(_, body), .. })
| Node::TraitItem(hir::TraitItem {
kind: hir::TraitItemKind::Fn(_, hir::TraitFn::Provided(body)),
..
}) => (Some(tcx.hir_body(*body)), &[][..]),
Node::TraitItem(hir::TraitItem {
kind: hir::TraitItemKind::Fn(_, hir::TraitFn::Required(names)),
..
})
| Node::ForeignItem(hir::ForeignItem {
kind: hir::ForeignItemKind::Fn(_, names, _), ..
}) => (None, *names),
_ => return None,
};
let ident = |index: usize| match body {
Some(body) => body.params.get(index).and_then(|param| match param.pat.kind {
hir::PatKind::Binding(_, _, ident, None) => Some(ident),
_ => None,
}),
None => names.get(index).copied().flatten(),
};
let pat = |index: usize| match body.and_then(|body| body.params.get(index)) {
Some(param) => source_text(tcx, param.pat.span).unwrap_or_else(|| {
let ann: &dyn crate::rustc_hir::intravisit::HirTyCtxt<'_> = &tcx;
crate::rustc_hir_pretty::pat_to_string(&ann, param.pat)
}),
None => ident(index).map_or_else(|| "_".to_string(), |ident| ident.as_str().to_string()),
};
let decl = sig.decl;
let receiver = match decl.implicit_self() {
hir::ImplicitSelfKind::Imm | hir::ImplicitSelfKind::Mut => {
Some(Receiver { kind: ReceiverKind::Value, ty: None })
}
hir::ImplicitSelfKind::RefImm => Some(Receiver { kind: ReceiverKind::Ref, ty: None }),
hir::ImplicitSelfKind::RefMut => Some(Receiver { kind: ReceiverKind::RefMut, ty: None }),
hir::ImplicitSelfKind::None => match (ident(0), decl.inputs.first()) {
(Some(ident), Some(ty)) if ident.name == crate::rustc_span::kw::SelfLower => {
Some(Receiver { kind: ReceiverKind::Typed, ty: Some(type_text(tcx, ty)) })
}
_ => None,
},
};
let skip = usize::from(receiver.is_some());
let params = decl
.inputs
.iter()
.enumerate()
.skip(skip)
.map(|(index, ty)| FnParam { pat: pat(index), ty: type_text(tcx, ty) })
.collect();
let ret = match decl.output {
hir::FnRetTy::Return(ty) => Some(type_text(tcx, ty)).filter(|text| !text.is_empty()),
hir::FnRetTy::DefaultReturn(_) => None,
};
Some(FnSignature { receiver, params, ret })
}
fn source_text(tcx: TyCtxt<'_>, span: Span) -> Option<String> {
tcx.sess.source_map().span_to_snippet(span).ok()
}
fn type_text(tcx: TyCtxt<'_>, ty: &hir::Ty<'_>) -> String {
source_text(tcx, ty.span).unwrap_or_else(|| {
let ann: &dyn crate::rustc_hir::intravisit::HirTyCtxt<'_> = &tcx;
crate::rustc_hir_pretty::ty_to_string(&ann, ty)
})
}
pub fn analyze_source(crate_name: &str, source: &str) -> Result<CrateFacts, FatalError> {
analyze_source_with_sysroot(crate_name, source, None)
}
pub fn analyze_source_with_sysroot(
crate_name: &str,
source: &str,
sysroot: Option<&str>,
) -> Result<CrateFacts, FatalError> {
assert!(
crate::unwind_janky::unwinding_is_enabled(),
"analyze_source needs panic=unwind and a catcher installed through unwind_janky::install_catcher"
);
let mut opts = Options::default();
opts.crate_name = Some(crate_name.to_string());
opts.crate_types = alloc::vec![CrateType::Rlib];
opts.unstable_features = UnstableFeatures::Allow;
let named = sysroot
.map(eko::path::PathBuf::from)
.or_else(|| eko::env::var_os("FRONTEND_SYSROOT").map(eko::path::PathBuf::from));
let has_sysroot = named.is_some();
if has_sysroot {
opts.sysroot = Sysroot::new(named);
} else {
opts.unstable_opts.crate_attr.push("no_core".to_string());
opts.unstable_opts.crate_attr.push("feature(no_core)".to_string());
}
let rustc_version = {
#[cfg(feature = "force_pinned_sysroot")]
{
None
}
#[cfg(not(feature = "force_pinned_sysroot"))]
{
if has_sysroot {
rustc_version_of_sysroot(opts.sysroot.path()).or_else(host_rustc_version)
} else {
None
}
}
};
let text = alloc::sync::Arc::new(eko::thread::Mutex::new(String::new()));
let using_internal_features =
alloc::boxed::Box::leak(alloc::boxed::Box::new(AtomicBool::new(false)));
let config = Config {
opts,
input: Input::Str { name: FileName::anon_source_code(source), input: source.to_string() },
psess_created: Some(capture_diagnostics(&text)),
using_internal_features,
rustc_version,
};
let mut extracted: Option<CrateFacts> = None;
let finished = catch_fatal_errors(|| {
run_compiler(config, |compiler| {
let krate = parse(&compiler.sess);
create_and_enter_global_ctxt(compiler, krate, |tcx| extracted = Some(extract(tcx)))
})
});
let mut facts = extracted.ok_or(FatalError)?;
let (errors, _warnings) = split_diagnostics(&text.lock());
facts.diagnostics =
errors.into_iter().filter(|error| !error.starts_with("error: aborting due to")).collect();
facts.complete = facts.complete && finished.is_ok() && facts.diagnostics.is_empty();
Ok(facts)
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Checked {
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub fatal: bool,
}
impl Checked {
pub fn is_clean(&self) -> bool {
self.errors.is_empty() && !self.fatal
}
}
struct Sink(alloc::sync::Arc<eko::thread::Mutex<String>>);
impl core::fmt::Write for Sink {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
self.0.lock().push_str(s);
Ok(())
}
}
fn capture_diagnostics(
text: &alloc::sync::Arc<eko::thread::Mutex<String>>,
) -> alloc::boxed::Box<dyn FnOnce(&mut crate::rustc_session::parse::ParseSess) + Send> {
let sink = text.clone();
alloc::boxed::Box::new(move |psess: &mut crate::rustc_session::parse::ParseSess| {
let emitter = crate::rustc_errors::plain_emitter::PlainEmitter::new()
.sm(Some(psess.clone_source_map()))
.short_message(true)
.dst(alloc::boxed::Box::new(Sink(sink)));
psess.set_emitter(alloc::boxed::Box::new(emitter));
})
}
pub(crate) fn split_diagnostics(captured: &str) -> (Vec<String>, Vec<String>) {
fn flush(entry: Option<String>, errors: &mut Vec<String>, warnings: &mut Vec<String>) {
if let Some(entry) = entry {
if entry.starts_with("error") || entry.starts_with("internal compiler error") {
errors.push(entry);
} else if entry.starts_with("warning") {
warnings.push(entry);
}
}
}
let mut errors = Vec::new();
let mut warnings = Vec::new();
let mut current: Option<String> = None;
for line in captured.lines() {
if line.starts_with(' ') {
if let Some(entry) = current.as_mut() {
entry.push('\n');
entry.push_str(line);
}
} else {
flush(current.take(), &mut errors, &mut warnings);
current = Some(line.to_string());
}
}
flush(current.take(), &mut errors, &mut warnings);
(errors, warnings)
}
pub fn check_source(crate_name: &str, source: &str) -> Checked {
assert!(
crate::unwind_janky::unwinding_is_enabled(),
"check_source needs panic=unwind and a catcher installed through unwind_janky::install_catcher"
);
let mut opts = Options::default();
opts.crate_name = Some(crate_name.to_string());
opts.crate_types = alloc::vec![CrateType::Rlib];
opts.unstable_features = UnstableFeatures::Allow;
opts.unstable_opts.crate_attr.push("no_core".to_string());
opts.unstable_opts.crate_attr.push("feature(no_core)".to_string());
let text = alloc::sync::Arc::new(eko::thread::Mutex::new(String::new()));
let using_internal_features =
alloc::boxed::Box::leak(alloc::boxed::Box::new(AtomicBool::new(false)));
let config = Config {
opts,
input: Input::Str { name: FileName::anon_source_code(source), input: source.to_string() },
psess_created: Some(capture_diagnostics(&text)),
using_internal_features,
rustc_version: None,
};
let finished = catch_fatal_errors(|| {
run_compiler(config, |compiler| {
let krate = parse(&compiler.sess);
create_and_enter_global_ctxt(compiler, krate, |tcx| tcx.analysis(()))
})
});
let (errors, warnings) = split_diagnostics(&text.lock());
Checked { errors, warnings, fatal: finished.is_err() }
}
fn host_rustc_version() -> Option<alloc::string::String> {
let out = eko::command::Command::new("rustc").arg("--version").output()?;
if !out.success() {
return None;
}
let line = alloc::string::String::from_utf8(out.stdout).ok()?;
let line = line.trim();
let version = line.strip_prefix("rustc ").unwrap_or(line).trim();
(!version.is_empty()).then(|| version.to_string())
}
fn byte_span(tcx: TyCtxt<'_>, span: Span) -> ByteSpan {
let sm = tcx.sess.source_map();
let lo = sm.lookup_byte_offset(span.lo());
let hi = sm.lookup_byte_offset(span.hi());
ByteSpan {
file: lo.sf.name.prefer_local_unconditionally().to_string(),
start: lo.pos.0,
end: hi.pos.0,
}
}
struct RefVisitor<'a, 'tcx> {
tcx: TyCtxt<'tcx>,
typeck: &'tcx crate::rustc_middle::ty::TypeckResults<'tcx>,
from: &'a str,
refs: &'a mut Vec<Reference>,
}
impl<'a, 'tcx> Visitor<'tcx> for RefVisitor<'a, 'tcx> {
type NestedFilter = intravisit::IgnoreNested;
type Result = ();
fn visit_expr(&mut self, expr: &'tcx crate::rustc_hir::Expr<'tcx>) {
match expr.kind {
ExprKind::Path(ref qpath) => {
if let Some(def_id) = self.typeck.qpath_res(qpath, expr.hir_id).opt_def_id() {
self.push(def_id, RefKind::Path, expr.span);
}
}
ExprKind::MethodCall(_, _, _, _) => {
if let Some(def_id) = self.typeck.type_dependent_def_id(expr.hir_id) {
self.push(def_id, RefKind::Method, expr.span);
}
}
_ => {}
}
intravisit::walk_expr(self, expr);
}
}
impl<'a, 'tcx> RefVisitor<'a, 'tcx> {
fn push(&mut self, def_id: crate::rustc_hir::def_id::DefId, kind: RefKind, span: Span) {
self.refs.push(Reference {
from_def_path: self.from.to_string(),
to_def_path: self.tcx.def_path_str(def_id),
kind,
span: byte_span(self.tcx, span),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn named_kinds_are_facts_and_use_impl_are_not() {
assert_eq!(fact_kind(DefKind::Fn), Some(FactKind::Fn));
assert_eq!(fact_kind(DefKind::AssocFn), Some(FactKind::AssocFn));
assert_eq!(fact_kind(DefKind::Trait), Some(FactKind::Trait));
assert_eq!(fact_kind(DefKind::Use), None);
assert_eq!(fact_kind(DefKind::Impl { of_trait: true }), None);
assert_eq!(fact_kind(DefKind::TyParam), None);
}
#[test]
fn facts_without_the_tolerance_fields_read_as_complete() {
let json = r#"{"crate_name":"c","definitions":[],"imports":[],"impls":[],"references":[]}"#;
let facts: CrateFacts = serde_json::from_str(json).unwrap();
assert!(facts.complete);
assert!(facts.diagnostics.is_empty());
assert!(facts.unanalyzed_bodies.is_empty());
}
#[test]
fn definitions_without_shape_fields_round_trip() {
let json = r#"{"def_path":"m::f","name":"f","kind":"fn","span":{"file":"lib.rs","start":0,"end":9}}"#;
let definition: Definition = serde_json::from_str(json).unwrap();
assert_eq!(definition.signature, None);
assert!(definition.fields.is_empty());
assert!(definition.variants.is_empty());
assert_eq!(serde_json::to_string(&definition).unwrap(), json);
}
#[test]
fn impls_serialized_without_items_read_with_none() {
let json = r#"{"def_path":"m::<impl S>","self_type":"S","trait_def_path":null,"span":{"file":"lib.rs","start":0,"end":9}}"#;
let fact: Impl = serde_json::from_str(json).unwrap();
assert!(fact.items.is_empty());
}
#[test]
fn receiver_kinds_serialize_in_snake_case() {
let receiver = Receiver { kind: ReceiverKind::RefMut, ty: None };
assert_eq!(serde_json::to_string(&receiver).unwrap(), r#"{"kind":"ref_mut","ty":null}"#);
}
#[test]
fn captured_output_splits_into_errors_and_warnings_with_their_locations() {
let captured = "error[E0425]: cannot find value `x` in this scope\n --> src/lib.rs:1:14\nwarning: unused variable: `y`\nnote: a note on its own\nFor more information about this error, try `rustc --explain E0425`.\nerror: aborting due to 1 previous error\n";
let (errors, warnings) = split_diagnostics(captured);
assert_eq!(
errors,
vec![
"error[E0425]: cannot find value `x` in this scope\n --> src/lib.rs:1:14"
.to_string(),
"error: aborting due to 1 previous error".to_string(),
]
);
assert_eq!(warnings, vec!["warning: unused variable: `y`".to_string()]);
}
}