From 66884e318f2edfb0dd66d76a7d3b080d0dd9e4c5 Mon Sep 17 00:00:00 2001 From: Joseph Ryan Date: Mon, 3 Aug 2020 12:45:27 -0500 Subject: [PATCH 1/5] Add json backend Respond to comments and start adding tests Fix re-exports and extern-crate Add expected output tests Add restricted paths Format Fix: associated methods missing in output Ignore stripped items Mark stripped items removing them creates dangling references Fix tests and update conversions Don't panic if JSON backend fails to create a file Fix attribute error in JSON testsuite Move rustdoc-json to rustdoc/ This way it doesn't have to build rustc twice. Eventually it should probably get its own suite, like rustdoc-js, but that can be fixed in a follow-up. Small cleanups Don't prettify json before printing This fully halves the size of the emitted JSON. Add comments [BREAKING CHANGE] rename version -> crate_version [BREAKING CHANGE] rename source -> span Use exhaustive matches Don't qualify imports for DefId Rename clean::{ItemEnum -> ItemKind}, clean::Item::{inner -> kind} Fix Visibility conversion clean::Visability was changed here: https://github.com/rust-lang/rust/pull/77820/files#diff-df9f90aae0b7769e1eb6ea6f1d73c1c3f649e1ac48a20e169662a8930eb427beL1467-R1509 More churn catchup Format --- src/librustdoc/clean/mod.rs | 2 +- src/librustdoc/clean/types.rs | 41 ++ src/librustdoc/json/conversions.rs | 596 ++++++++++++++++++ src/librustdoc/json/mod.rs | 223 ++++++- src/librustdoc/json/types.rs | 493 +++++++++++++++ src/test/rustdoc/rustdoc-json/Makefile | 6 + .../rustdoc-json/check_missing_items.py | 181 ++++++ src/test/rustdoc/rustdoc-json/compare.py | 107 ++++ .../rustdoc/rustdoc-json/structs.expected | 468 ++++++++++++++ src/test/rustdoc/rustdoc-json/structs.rs | 17 + 10 files changed, 2117 insertions(+), 17 deletions(-) create mode 100644 src/librustdoc/json/conversions.rs create mode 100644 src/librustdoc/json/types.rs create mode 100644 src/test/rustdoc/rustdoc-json/Makefile create mode 100644 src/test/rustdoc/rustdoc-json/check_missing_items.py create mode 100644 src/test/rustdoc/rustdoc-json/compare.py create mode 100644 src/test/rustdoc/rustdoc-json/structs.expected create mode 100644 src/test/rustdoc/rustdoc-json/structs.rs diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index d294d8f02a80f..9d42705da82a5 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2281,7 +2281,7 @@ impl Clean> for doctree::Import<'_> { name: None, attrs: self.attrs.clean(cx), source: self.span.clean(cx), - def_id: DefId::local(CRATE_DEF_INDEX), + def_id: cx.tcx.hir().local_def_id(self.id).to_def_id(), visibility: self.vis.clean(cx), stability: None, const_stability: None, diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 38d25d8d98e85..d4796b7ed66ca 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -337,6 +337,42 @@ crate enum ItemKind { } impl ItemKind { + /// Some items contain others such as structs (for their fields) and Enums + /// (for their variants). This method returns those contained items. + crate fn inner_items(&self) -> impl Iterator { + match self { + StructItem(s) => s.fields.iter(), + UnionItem(u) => u.fields.iter(), + VariantItem(Variant { kind: VariantKind::Struct(v) }) => v.fields.iter(), + EnumItem(e) => e.variants.iter(), + TraitItem(t) => t.items.iter(), + ImplItem(i) => i.items.iter(), + ModuleItem(m) => m.items.iter(), + ExternCrateItem(_, _) + | ImportItem(_) + | FunctionItem(_) + | TypedefItem(_, _) + | OpaqueTyItem(_) + | StaticItem(_) + | ConstantItem(_) + | TraitAliasItem(_) + | TyMethodItem(_) + | MethodItem(_, _) + | StructFieldItem(_) + | VariantItem(_) + | ForeignFunctionItem(_) + | ForeignStaticItem(_) + | ForeignTypeItem + | MacroItem(_) + | ProcMacroItem(_) + | PrimitiveItem(_) + | AssocConstItem(_, _) + | AssocTypeItem(_, _) + | StrippedItem(_) + | KeywordItem(_) => [].iter(), + } + } + crate fn is_type_alias(&self) -> bool { match *self { ItemKind::TypedefItem(_, _) | ItemKind::AssocTypeItem(_, _) => true, @@ -1613,6 +1649,11 @@ impl Path { crate fn last_name(&self) -> &str { self.segments.last().expect("segments were empty").name.as_str() } + + crate fn whole_name(&self) -> String { + String::from(if self.global { "::" } else { "" }) + + &self.segments.iter().map(|s| s.name.clone()).collect::>().join("::") + } } #[derive(Clone, PartialEq, Eq, Debug, Hash)] diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs new file mode 100644 index 0000000000000..80192623d54fe --- /dev/null +++ b/src/librustdoc/json/conversions.rs @@ -0,0 +1,596 @@ +//! These from impls are used to create the JSON types which get serialized. They're very close to +//! the `clean` types but with some fields removed or stringified to simplify the output and not +//! expose unstable compiler internals. + +use std::convert::From; + +use rustc_ast::ast; +use rustc_span::def_id::{DefId, CRATE_DEF_INDEX}; + +use crate::clean; +use crate::doctree; +use crate::formats::item_type::ItemType; +use crate::json::types::*; + +impl From for Item { + fn from(item: clean::Item) -> Self { + let item_type = ItemType::from(&item); + let clean::Item { + source, + name, + attrs, + kind: inner, + visibility, + def_id, + stability: _, + deprecation, + } = item; + Item { + id: def_id.into(), + crate_id: def_id.krate.as_u32(), + name, + stripped: match inner { + clean::StrippedItem(_) => true, + _ => false, + }, + source: source.into(), + visibility: visibility.into(), + docs: attrs.collapsed_doc_value().unwrap_or_default(), + links: attrs + .links + .into_iter() + .filter_map(|clean::ItemLink { link, did, .. }| did.map(|did| (link, did.into()))) + .collect(), + attrs: attrs + .other_attrs + .iter() + .map(rustc_ast_pretty::pprust::attribute_to_string) + .collect(), + deprecation: deprecation.map(Into::into), + kind: item_type.into(), + inner: inner.into(), + } + } +} + +impl From for Option { + fn from(span: clean::Span) -> Self { + let clean::Span { loline, locol, hiline, hicol, .. } = span; + match span.filename { + rustc_span::FileName::Real(name) => Some(Span { + filename: match name { + rustc_span::RealFileName::Named(path) => path, + rustc_span::RealFileName::Devirtualized { local_path, virtual_name: _ } => { + local_path + } + }, + begin: (loline, locol), + end: (hiline, hicol), + }), + _ => None, + } + } +} + +impl From for Deprecation { + fn from(deprecation: clean::Deprecation) -> Self { + let clean::Deprecation { since, note, is_since_rustc_version: _ } = deprecation; + Deprecation { since, note } + } +} + +impl From for Visibility { + fn from(v: clean::Visibility) -> Self { + use clean::Visibility::*; + match v { + Public => Visibility::Public, + Inherited => Visibility::Default, + Restricted(did, _) if did.index == CRATE_DEF_INDEX => Visibility::Crate, + Restricted(did, path) => Visibility::Restricted { + parent: did.into(), + path: path.to_string_no_crate_verbose(), + }, + } + } +} + +impl From for GenericArgs { + fn from(args: clean::GenericArgs) -> Self { + use clean::GenericArgs::*; + match args { + AngleBracketed { args, bindings } => GenericArgs::AngleBracketed { + args: args.into_iter().map(Into::into).collect(), + bindings: bindings.into_iter().map(Into::into).collect(), + }, + Parenthesized { inputs, output } => GenericArgs::Parenthesized { + inputs: inputs.into_iter().map(Into::into).collect(), + output: output.map(Into::into), + }, + } + } +} + +impl From for GenericArg { + fn from(arg: clean::GenericArg) -> Self { + use clean::GenericArg::*; + match arg { + Lifetime(l) => GenericArg::Lifetime(l.0), + Type(t) => GenericArg::Type(t.into()), + Const(c) => GenericArg::Const(c.into()), + } + } +} + +impl From for Constant { + fn from(constant: clean::Constant) -> Self { + let clean::Constant { type_, expr, value, is_literal } = constant; + Constant { type_: type_.into(), expr, value, is_literal } + } +} + +impl From for TypeBinding { + fn from(binding: clean::TypeBinding) -> Self { + TypeBinding { name: binding.name, binding: binding.kind.into() } + } +} + +impl From for TypeBindingKind { + fn from(kind: clean::TypeBindingKind) -> Self { + use clean::TypeBindingKind::*; + match kind { + Equality { ty } => TypeBindingKind::Equality(ty.into()), + Constraint { bounds } => { + TypeBindingKind::Constraint(bounds.into_iter().map(Into::into).collect()) + } + } + } +} + +impl From for Id { + fn from(did: DefId) -> Self { + Id(format!("{}:{}", did.krate.as_u32(), u32::from(did.index))) + } +} + +impl From for ItemEnum { + fn from(item: clean::ItemKind) -> Self { + use clean::ItemKind::*; + match item { + ModuleItem(m) => ItemEnum::ModuleItem(m.into()), + ExternCrateItem(c, a) => ItemEnum::ExternCrateItem { name: c, rename: a }, + ImportItem(i) => ItemEnum::ImportItem(i.into()), + StructItem(s) => ItemEnum::StructItem(s.into()), + UnionItem(u) => ItemEnum::StructItem(u.into()), + StructFieldItem(f) => ItemEnum::StructFieldItem(f.into()), + EnumItem(e) => ItemEnum::EnumItem(e.into()), + VariantItem(v) => ItemEnum::VariantItem(v.into()), + FunctionItem(f) => ItemEnum::FunctionItem(f.into()), + ForeignFunctionItem(f) => ItemEnum::FunctionItem(f.into()), + TraitItem(t) => ItemEnum::TraitItem(t.into()), + TraitAliasItem(t) => ItemEnum::TraitAliasItem(t.into()), + MethodItem(m, _) => ItemEnum::MethodItem(m.into()), + TyMethodItem(m) => ItemEnum::MethodItem(m.into()), + ImplItem(i) => ItemEnum::ImplItem(i.into()), + StaticItem(s) => ItemEnum::StaticItem(s.into()), + ForeignStaticItem(s) => ItemEnum::StaticItem(s.into()), + ForeignTypeItem => ItemEnum::ForeignTypeItem, + TypedefItem(t, _) => ItemEnum::TypedefItem(t.into()), + OpaqueTyItem(t) => ItemEnum::OpaqueTyItem(t.into()), + ConstantItem(c) => ItemEnum::ConstantItem(c.into()), + MacroItem(m) => ItemEnum::MacroItem(m.source), + ProcMacroItem(m) => ItemEnum::ProcMacroItem(m.into()), + AssocConstItem(t, s) => ItemEnum::AssocConstItem { type_: t.into(), default: s }, + AssocTypeItem(g, t) => ItemEnum::AssocTypeItem { + bounds: g.into_iter().map(Into::into).collect(), + default: t.map(Into::into), + }, + StrippedItem(inner) => (*inner).into(), + PrimitiveItem(_) | KeywordItem(_) => { + panic!("{:?} is not supported for JSON output", item) + } + } + } +} + +impl From for Module { + fn from(module: clean::Module) -> Self { + Module { + is_crate: module.is_crate, + items: module.items.into_iter().map(|i| i.def_id.into()).collect(), + } + } +} + +impl From for Struct { + fn from(struct_: clean::Struct) -> Self { + let clean::Struct { struct_type, generics, fields, fields_stripped } = struct_; + Struct { + struct_type: struct_type.into(), + generics: generics.into(), + fields_stripped, + fields: fields.into_iter().map(|i| i.def_id.into()).collect(), + impls: Vec::new(), // Added in JsonRenderer::item + } + } +} + +impl From for Struct { + fn from(struct_: clean::Union) -> Self { + let clean::Union { struct_type, generics, fields, fields_stripped } = struct_; + Struct { + struct_type: struct_type.into(), + generics: generics.into(), + fields_stripped, + fields: fields.into_iter().map(|i| i.def_id.into()).collect(), + impls: Vec::new(), // Added in JsonRenderer::item + } + } +} + +impl From for StructType { + fn from(struct_type: doctree::StructType) -> Self { + use doctree::StructType::*; + match struct_type { + Plain => StructType::Plain, + Tuple => StructType::Tuple, + Unit => StructType::Unit, + } + } +} + +fn stringify_header(header: &rustc_hir::FnHeader) -> String { + let mut s = String::from(header.unsafety.prefix_str()); + if header.asyncness == rustc_hir::IsAsync::Async { + s.push_str("async ") + } + if header.constness == rustc_hir::Constness::Const { + s.push_str("const ") + } + s +} + +impl From for Function { + fn from(function: clean::Function) -> Self { + let clean::Function { decl, generics, header, all_types: _, ret_types: _ } = function; + Function { + decl: decl.into(), + generics: generics.into(), + header: stringify_header(&header), + abi: header.abi.to_string(), + } + } +} + +impl From for Generics { + fn from(generics: clean::Generics) -> Self { + Generics { + params: generics.params.into_iter().map(Into::into).collect(), + where_predicates: generics.where_predicates.into_iter().map(Into::into).collect(), + } + } +} + +impl From for GenericParamDef { + fn from(generic_param: clean::GenericParamDef) -> Self { + GenericParamDef { name: generic_param.name, kind: generic_param.kind.into() } + } +} + +impl From for GenericParamDefKind { + fn from(kind: clean::GenericParamDefKind) -> Self { + use clean::GenericParamDefKind::*; + match kind { + Lifetime => GenericParamDefKind::Lifetime, + Type { did: _, bounds, default, synthetic: _ } => GenericParamDefKind::Type { + bounds: bounds.into_iter().map(Into::into).collect(), + default: default.map(Into::into), + }, + Const { did: _, ty } => GenericParamDefKind::Const(ty.into()), + } + } +} + +impl From for WherePredicate { + fn from(predicate: clean::WherePredicate) -> Self { + use clean::WherePredicate::*; + match predicate { + BoundPredicate { ty, bounds } => WherePredicate::BoundPredicate { + ty: ty.into(), + bounds: bounds.into_iter().map(Into::into).collect(), + }, + RegionPredicate { lifetime, bounds } => WherePredicate::RegionPredicate { + lifetime: lifetime.0, + bounds: bounds.into_iter().map(Into::into).collect(), + }, + EqPredicate { lhs, rhs } => { + WherePredicate::EqPredicate { lhs: lhs.into(), rhs: rhs.into() } + } + } + } +} + +impl From for GenericBound { + fn from(bound: clean::GenericBound) -> Self { + use clean::GenericBound::*; + match bound { + TraitBound(clean::PolyTrait { trait_, generic_params }, modifier) => { + GenericBound::TraitBound { + trait_: trait_.into(), + generic_params: generic_params.into_iter().map(Into::into).collect(), + modifier: modifier.into(), + } + } + Outlives(lifetime) => GenericBound::Outlives(lifetime.0), + } + } +} + +impl From for TraitBoundModifier { + fn from(modifier: rustc_hir::TraitBoundModifier) -> Self { + use rustc_hir::TraitBoundModifier::*; + match modifier { + None => TraitBoundModifier::None, + Maybe => TraitBoundModifier::Maybe, + MaybeConst => TraitBoundModifier::MaybeConst, + } + } +} + +impl From for Type { + fn from(ty: clean::Type) -> Self { + use clean::Type::*; + match ty { + ResolvedPath { path, param_names, did, is_generic: _ } => Type::ResolvedPath { + name: path.whole_name(), + id: did.into(), + args: path.segments.last().map(|args| Box::new(args.clone().args.into())), + param_names: param_names + .map(|v| v.into_iter().map(Into::into).collect()) + .unwrap_or_default(), + }, + Generic(s) => Type::Generic(s), + Primitive(p) => Type::Primitive(p.as_str().to_string()), + BareFunction(f) => Type::FunctionPointer(Box::new((*f).into())), + Tuple(t) => Type::Tuple(t.into_iter().map(Into::into).collect()), + Slice(t) => Type::Slice(Box::new((*t).into())), + Array(t, s) => Type::Array { type_: Box::new((*t).into()), len: s }, + ImplTrait(g) => Type::ImplTrait(g.into_iter().map(Into::into).collect()), + Never => Type::Never, + Infer => Type::Infer, + RawPointer(mutability, type_) => Type::RawPointer { + mutable: mutability == ast::Mutability::Mut, + type_: Box::new((*type_).into()), + }, + BorrowedRef { lifetime, mutability, type_ } => Type::BorrowedRef { + lifetime: lifetime.map(|l| l.0), + mutable: mutability == ast::Mutability::Mut, + type_: Box::new((*type_).into()), + }, + QPath { name, self_type, trait_ } => Type::QualifiedPath { + name, + self_type: Box::new((*self_type).into()), + trait_: Box::new((*trait_).into()), + }, + } + } +} + +impl From for FunctionPointer { + fn from(bare_decl: clean::BareFunctionDecl) -> Self { + let clean::BareFunctionDecl { unsafety, generic_params, decl, abi } = bare_decl; + FunctionPointer { + is_unsafe: unsafety == rustc_hir::Unsafety::Unsafe, + generic_params: generic_params.into_iter().map(Into::into).collect(), + decl: decl.into(), + abi: abi.to_string(), + } + } +} + +impl From for FnDecl { + fn from(decl: clean::FnDecl) -> Self { + let clean::FnDecl { inputs, output, c_variadic, attrs: _ } = decl; + FnDecl { + inputs: inputs.values.into_iter().map(|arg| (arg.name, arg.type_.into())).collect(), + output: match output { + clean::FnRetTy::Return(t) => Some(t.into()), + clean::FnRetTy::DefaultReturn => None, + }, + c_variadic, + } + } +} + +impl From for Trait { + fn from(trait_: clean::Trait) -> Self { + let clean::Trait { unsafety, items, generics, bounds, is_spotlight: _, is_auto } = trait_; + Trait { + is_auto, + is_unsafe: unsafety == rustc_hir::Unsafety::Unsafe, + items: items.into_iter().map(|i| i.def_id.into()).collect(), + generics: generics.into(), + bounds: bounds.into_iter().map(Into::into).collect(), + implementors: Vec::new(), // Added in JsonRenderer::item + } + } +} + +impl From for Impl { + fn from(impl_: clean::Impl) -> Self { + let clean::Impl { + unsafety, + generics, + provided_trait_methods, + trait_, + for_, + items, + polarity, + synthetic, + blanket_impl, + } = impl_; + Impl { + is_unsafe: unsafety == rustc_hir::Unsafety::Unsafe, + generics: generics.into(), + provided_trait_methods: provided_trait_methods.into_iter().collect(), + trait_: trait_.map(Into::into), + for_: for_.into(), + items: items.into_iter().map(|i| i.def_id.into()).collect(), + negative: polarity == Some(clean::ImplPolarity::Negative), + synthetic, + blanket_impl: blanket_impl.map(Into::into), + } + } +} + +impl From for Method { + fn from(function: clean::Function) -> Self { + let clean::Function { header, decl, generics, all_types: _, ret_types: _ } = function; + Method { + decl: decl.into(), + generics: generics.into(), + header: stringify_header(&header), + has_body: true, + } + } +} + +impl From for Enum { + fn from(enum_: clean::Enum) -> Self { + let clean::Enum { variants, generics, variants_stripped } = enum_; + Enum { + generics: generics.into(), + variants_stripped, + variants: variants.into_iter().map(|i| i.def_id.into()).collect(), + impls: Vec::new(), // Added in JsonRenderer::item + } + } +} + +impl From for Struct { + fn from(struct_: clean::VariantStruct) -> Self { + let clean::VariantStruct { struct_type, fields, fields_stripped } = struct_; + Struct { + struct_type: struct_type.into(), + generics: Default::default(), + fields_stripped, + fields: fields.into_iter().map(|i| i.def_id.into()).collect(), + impls: Vec::new(), + } + } +} + +impl From for Variant { + fn from(variant: clean::Variant) -> Self { + use clean::VariantKind::*; + match variant.kind { + CLike => Variant::Plain, + Tuple(t) => Variant::Tuple(t.into_iter().map(Into::into).collect()), + Struct(s) => Variant::Struct(s.fields.into_iter().map(|i| i.def_id.into()).collect()), + } + } +} + +impl From for Import { + fn from(import: clean::Import) -> Self { + use clean::ImportKind::*; + match import.kind { + Simple(s) => Import { + span: import.source.path.whole_name(), + name: s, + id: import.source.did.map(Into::into), + glob: false, + }, + Glob => Import { + span: import.source.path.whole_name(), + name: import.source.path.last_name().to_string(), + id: import.source.did.map(Into::into), + glob: true, + }, + } + } +} + +impl From for ProcMacro { + fn from(mac: clean::ProcMacro) -> Self { + ProcMacro { kind: mac.kind.into(), helpers: mac.helpers } + } +} + +impl From for MacroKind { + fn from(kind: rustc_span::hygiene::MacroKind) -> Self { + use rustc_span::hygiene::MacroKind::*; + match kind { + Bang => MacroKind::Bang, + Attr => MacroKind::Attr, + Derive => MacroKind::Derive, + } + } +} + +impl From for Typedef { + fn from(typedef: clean::Typedef) -> Self { + let clean::Typedef { type_, generics, item_type: _ } = typedef; + Typedef { type_: type_.into(), generics: generics.into() } + } +} + +impl From for OpaqueTy { + fn from(opaque: clean::OpaqueTy) -> Self { + OpaqueTy { + bounds: opaque.bounds.into_iter().map(Into::into).collect(), + generics: opaque.generics.into(), + } + } +} + +impl From for Static { + fn from(stat: clean::Static) -> Self { + Static { + type_: stat.type_.into(), + mutable: stat.mutability == ast::Mutability::Mut, + expr: stat.expr, + } + } +} + +impl From for TraitAlias { + fn from(alias: clean::TraitAlias) -> Self { + TraitAlias { + generics: alias.generics.into(), + params: alias.bounds.into_iter().map(Into::into).collect(), + } + } +} + +impl From for ItemKind { + fn from(kind: ItemType) -> Self { + use ItemType::*; + match kind { + Module => ItemKind::Module, + ExternCrate => ItemKind::ExternCrate, + Import => ItemKind::Import, + Struct => ItemKind::Struct, + Union => ItemKind::Union, + Enum => ItemKind::Enum, + Function => ItemKind::Function, + Typedef => ItemKind::Typedef, + OpaqueTy => ItemKind::OpaqueTy, + Static => ItemKind::Static, + Constant => ItemKind::Constant, + Trait => ItemKind::Trait, + Impl => ItemKind::Impl, + TyMethod | Method => ItemKind::Method, + StructField => ItemKind::StructField, + Variant => ItemKind::Variant, + Macro => ItemKind::Macro, + Primitive => ItemKind::Primitive, + AssocConst => ItemKind::AssocConst, + AssocType => ItemKind::AssocType, + ForeignType => ItemKind::ForeignType, + Keyword => ItemKind::Keyword, + TraitAlias => ItemKind::TraitAlias, + ProcAttribute => ItemKind::ProcAttribute, + ProcDerive => ItemKind::ProcDerive, + } + } +} diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index 5eb1f7b1f7780..20a8de144adb3 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -1,47 +1,238 @@ +//! Rustdoc's JSON backend +//! +//! This module contains the logic for rendering a crate as JSON rather than the normal static HTML +//! output. See [the RFC](https://github.com/rust-lang/rfcs/pull/2963) and the [`types`] module +//! docs for usage and details. + +mod conversions; +pub mod types; + +use std::cell::RefCell; +use std::fs::File; +use std::path::PathBuf; +use std::rc::Rc; + +use rustc_data_structures::fx::FxHashMap; +use rustc_span::edition::Edition; + use crate::clean; use crate::config::{RenderInfo, RenderOptions}; use crate::error::Error; use crate::formats::cache::Cache; use crate::formats::FormatRenderer; - -use rustc_span::edition::Edition; +use crate::html::render::cache::ExternalLocation; #[derive(Clone)] -crate struct JsonRenderer {} +crate struct JsonRenderer { + /// A mapping of IDs that contains all local items for this crate which gets output as a top + /// level field of the JSON blob. + index: Rc>>, + /// The directory where the blob will be written to. + out_path: PathBuf, +} + +impl JsonRenderer { + fn get_trait_implementors( + &mut self, + id: rustc_span::def_id::DefId, + cache: &Cache, + ) -> Vec { + cache + .implementors + .get(&id) + .map(|implementors| { + implementors + .iter() + .map(|i| { + let item = &i.impl_item; + self.item(item.clone(), cache).unwrap(); + item.def_id.into() + }) + .collect() + }) + .unwrap_or_default() + } + + fn get_impls(&mut self, id: rustc_span::def_id::DefId, cache: &Cache) -> Vec { + cache + .impls + .get(&id) + .map(|impls| { + impls + .iter() + .filter_map(|i| { + let item = &i.impl_item; + if item.def_id.is_local() { + self.item(item.clone(), cache).unwrap(); + Some(item.def_id.into()) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() + } + + fn get_trait_items(&mut self, cache: &Cache) -> Vec<(types::Id, types::Item)> { + cache + .traits + .iter() + .filter_map(|(&id, trait_item)| { + // only need to synthesize items for external traits + if !id.is_local() { + trait_item.items.clone().into_iter().for_each(|i| self.item(i, cache).unwrap()); + Some(( + id.into(), + types::Item { + id: id.into(), + crate_id: id.krate.as_u32(), + name: cache + .paths + .get(&id) + .unwrap_or_else(|| { + cache + .external_paths + .get(&id) + .expect("Trait should either be in local or external paths") + }) + .0 + .last() + .map(Clone::clone), + stripped: false, + visibility: types::Visibility::Public, + kind: types::ItemKind::Trait, + inner: types::ItemEnum::TraitItem(trait_item.clone().into()), + source: None, + docs: Default::default(), + links: Default::default(), + attrs: Default::default(), + deprecation: Default::default(), + }, + )) + } else { + None + } + }) + .collect() + } +} impl FormatRenderer for JsonRenderer { fn init( - _krate: clean::Crate, - _options: RenderOptions, + krate: clean::Crate, + options: RenderOptions, _render_info: RenderInfo, _edition: Edition, _cache: &mut Cache, ) -> Result<(Self, clean::Crate), Error> { - unimplemented!() + debug!("Initializing json renderer"); + Ok(( + JsonRenderer { + index: Rc::new(RefCell::new(FxHashMap::default())), + out_path: options.output, + }, + krate, + )) } - fn item(&mut self, _item: clean::Item, _cache: &Cache) -> Result<(), Error> { - unimplemented!() + /// Inserts an item into the index. This should be used rather than directly calling insert on + /// the hashmap because certain items (traits and types) need to have their mappings for trait + /// implementations filled out before they're inserted. + fn item(&mut self, item: clean::Item, cache: &Cache) -> Result<(), Error> { + // Flatten items that recursively store other items + item.kind.inner_items().for_each(|i| self.item(i.clone(), cache).unwrap()); + + let id = item.def_id; + let mut new_item: types::Item = item.into(); + if let types::ItemEnum::TraitItem(ref mut t) = new_item.inner { + t.implementors = self.get_trait_implementors(id, cache) + } else if let types::ItemEnum::StructItem(ref mut s) = new_item.inner { + s.impls = self.get_impls(id, cache) + } else if let types::ItemEnum::EnumItem(ref mut e) = new_item.inner { + e.impls = self.get_impls(id, cache) + } + + self.index.borrow_mut().insert(id.into(), new_item); + Ok(()) } fn mod_item_in( &mut self, - _item: &clean::Item, - _item_name: &str, - _cache: &Cache, + item: &clean::Item, + _module_name: &str, + cache: &Cache, ) -> Result<(), Error> { - unimplemented!() + use clean::types::ItemKind::*; + if let ModuleItem(m) = &item.kind { + for item in &m.items { + match &item.kind { + // These don't have names so they don't get added to the output by default + ImportItem(_) => self.item(item.clone(), cache).unwrap(), + ExternCrateItem(_, _) => self.item(item.clone(), cache).unwrap(), + ImplItem(i) => { + i.items.iter().for_each(|i| self.item(i.clone(), cache).unwrap()) + } + _ => {} + } + } + } + self.item(item.clone(), cache).unwrap(); + Ok(()) } fn mod_item_out(&mut self, _item_name: &str) -> Result<(), Error> { - unimplemented!() + Ok(()) } - fn after_krate(&mut self, _krate: &clean::Crate, _cache: &Cache) -> Result<(), Error> { - unimplemented!() + fn after_krate(&mut self, krate: &clean::Crate, cache: &Cache) -> Result<(), Error> { + debug!("Done with crate"); + let mut index = (*self.index).clone().into_inner(); + index.extend(self.get_trait_items(cache)); + let output = types::Crate { + root: types::Id(String::from("0:0")), + crate_version: krate.version.clone(), + includes_private: cache.document_private, + index, + paths: cache + .paths + .clone() + .into_iter() + .chain(cache.external_paths.clone().into_iter()) + .map(|(k, (path, kind))| { + ( + k.into(), + types::ItemSummary { crate_id: k.krate.as_u32(), path, kind: kind.into() }, + ) + }) + .collect(), + external_crates: cache + .extern_locations + .iter() + .map(|(k, v)| { + ( + k.as_u32(), + types::ExternalCrate { + name: v.0.clone(), + html_root_url: match &v.2 { + ExternalLocation::Remote(s) => Some(s.clone()), + _ => None, + }, + }, + ) + }) + .collect(), + format_version: 1, + }; + let mut p = self.out_path.clone(); + p.push(output.index.get(&output.root).unwrap().name.clone().unwrap()); + p.set_extension("json"); + let file = File::create(&p).map_err(|error| Error { error: error.to_string(), file: p })?; + serde_json::ser::to_writer(&file, &output).unwrap(); + Ok(()) } fn after_run(&mut self, _diag: &rustc_errors::Handler) -> Result<(), Error> { - unimplemented!() + Ok(()) } } diff --git a/src/librustdoc/json/types.rs b/src/librustdoc/json/types.rs new file mode 100644 index 0000000000000..62705affafce1 --- /dev/null +++ b/src/librustdoc/json/types.rs @@ -0,0 +1,493 @@ +//! Rustdoc's JSON output interface +//! +//! These types are the public API exposed through the `--output-format json` flag. The [`Crate`] +//! struct is the root of the JSON blob and all other items are contained within. + +use std::path::PathBuf; + +use rustc_data_structures::fx::FxHashMap; +use serde::{Deserialize, Serialize}; + +/// A `Crate` is the root of the emitted JSON blob. It contains all type/documentation information +/// about the language items in the local crate, as well as info about external items to allow +/// tools to find or link to them. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Crate { + /// The id of the root [`Module`] item of the local crate. + pub root: Id, + /// The version string given to `--crate-version`, if any. + pub crate_version: Option, + /// Whether or not the output includes private items. + pub includes_private: bool, + /// A collection of all items in the local crate as well as some external traits and their + /// items that are referenced locally. + pub index: FxHashMap, + /// Maps IDs to fully qualified paths and other info helpful for generating links. + pub paths: FxHashMap, + /// Maps `crate_id` of items to a crate name and html_root_url if it exists. + pub external_crates: FxHashMap, + /// A single version number to be used in the future when making backwards incompatible changes + /// to the JSON output. + pub format_version: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ExternalCrate { + pub name: String, + pub html_root_url: Option, +} + +/// For external (not defined in the local crate) items, you don't get the same level of +/// information. This struct should contain enough to generate a link/reference to the item in +/// question, or can be used by a tool that takes the json output of multiple crates to find +/// the actual item definition with all the relevant info. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ItemSummary { + /// Can be used to look up the name and html_root_url of the crate this item came from in the + /// `external_crates` map. + pub crate_id: u32, + /// The list of path components for the fully qualified path of this item (e.g. + /// `["std", "io", "lazy", "Lazy"]` for `std::io::lazy::Lazy`). + pub path: Vec, + /// Whether this item is a struct, trait, macro, etc. + pub kind: ItemKind, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Item { + /// The unique identifier of this item. Can be used to find this item in various mappings. + pub id: Id, + /// This can be used as a key to the `external_crates` map of [`Crate`] to see which crate + /// this item came from. + pub crate_id: u32, + /// Some items such as impls don't have names. + pub name: Option, + /// Whether this item is meant to be omitted from the generated documentation due to `#doc(hidden)`, + /// because it is private, or because it was inlined. + pub stripped: bool, + /// The source location of this item (absent if it came from a macro expansion or inline + /// assembly). + pub source: Option, + /// By default all documented items are public, but you can tell rustdoc to output private items + /// so this field is needed to differentiate. + pub visibility: Visibility, + /// The full markdown docstring of this item. + pub docs: String, + /// This mapping resolves [intra-doc links](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md) from the docstring to their IDs + pub links: FxHashMap, + /// Stringified versions of the attributes on this item (e.g. `"#[inline]"`) + pub attrs: Vec, + pub deprecation: Option, + pub kind: ItemKind, + pub inner: ItemEnum, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Span { + /// The path to the source file for this span relative to the path `rustdoc` was invoked with. + pub filename: PathBuf, + /// Zero indexed Line and Column of the first character of the `Span` + pub begin: (usize, usize), + /// Zero indexed Line and Column of the last character of the `Span` + pub end: (usize, usize), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Deprecation { + pub since: Option, + pub note: Option, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Visibility { + Public, + /// For the most part items are private by default. The exceptions are associated items of + /// public traits and variants of public enums. + Default, + Crate, + /// For `pub(in path)` visibility. `parent` is the module it's restricted to and `path` is how + /// that module was referenced (like `"super::super"` or `"crate::foo::bar"`). + Restricted { + parent: Id, + path: String, + }, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum GenericArgs { + /// <'a, 32, B: Copy, C = u32> + AngleBracketed { args: Vec, bindings: Vec }, + /// Fn(A, B) -> C + Parenthesized { inputs: Vec, output: Option }, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum GenericArg { + Lifetime(String), + Type(Type), + Const(Constant), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Constant { + #[serde(rename = "type")] + pub type_: Type, + pub expr: String, + pub value: Option, + pub is_literal: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TypeBinding { + pub name: String, + pub binding: TypeBindingKind, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum TypeBindingKind { + Equality(Type), + Constraint(Vec), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Id(pub String); + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ItemKind { + Module, + ExternCrate, + Import, + Struct, + StructField, + Union, + Enum, + Variant, + Function, + Typedef, + OpaqueTy, + Constant, + Trait, + TraitAlias, + Method, + Impl, + Static, + ForeignType, + Macro, + ProcAttribute, + ProcDerive, + AssocConst, + AssocType, + Primitive, + Keyword, +} + +#[serde(untagged)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ItemEnum { + ModuleItem(Module), + ExternCrateItem { + name: String, + rename: Option, + }, + ImportItem(Import), + + StructItem(Struct), + StructFieldItem(Type), + EnumItem(Enum), + VariantItem(Variant), + + FunctionItem(Function), + + TraitItem(Trait), + TraitAliasItem(TraitAlias), + MethodItem(Method), + ImplItem(Impl), + + TypedefItem(Typedef), + OpaqueTyItem(OpaqueTy), + ConstantItem(Constant), + + StaticItem(Static), + + /// `type`s from an extern block + ForeignTypeItem, + + /// Declarative macro_rules! macro + MacroItem(String), + ProcMacroItem(ProcMacro), + + AssocConstItem { + #[serde(rename = "type")] + type_: Type, + /// e.g. `const X: usize = 5;` + default: Option, + }, + AssocTypeItem { + bounds: Vec, + /// e.g. `type X = usize;` + default: Option, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Module { + pub is_crate: bool, + pub items: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Struct { + pub struct_type: StructType, + pub generics: Generics, + pub fields_stripped: bool, + pub fields: Vec, + pub impls: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Enum { + pub generics: Generics, + pub variants_stripped: bool, + pub variants: Vec, + pub impls: Vec, +} + +#[serde(rename_all = "snake_case")] +#[serde(tag = "variant_kind", content = "variant_inner")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Variant { + Plain, + Tuple(Vec), + Struct(Vec), +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum StructType { + Plain, + Tuple, + Unit, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Function { + pub decl: FnDecl, + pub generics: Generics, + pub header: String, + pub abi: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Method { + pub decl: FnDecl, + pub generics: Generics, + pub header: String, + pub has_body: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Generics { + pub params: Vec, + pub where_predicates: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GenericParamDef { + pub name: String, + pub kind: GenericParamDefKind, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum GenericParamDefKind { + Lifetime, + Type { bounds: Vec, default: Option }, + Const(Type), +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum WherePredicate { + BoundPredicate { ty: Type, bounds: Vec }, + RegionPredicate { lifetime: String, bounds: Vec }, + EqPredicate { lhs: Type, rhs: Type }, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum GenericBound { + TraitBound { + #[serde(rename = "trait")] + trait_: Type, + /// Used for HRTBs + generic_params: Vec, + modifier: TraitBoundModifier, + }, + Outlives(String), +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum TraitBoundModifier { + None, + Maybe, + MaybeConst, +} + +#[serde(rename_all = "snake_case")] +#[serde(tag = "kind", content = "inner")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Type { + /// Structs, enums, and traits + ResolvedPath { + name: String, + id: Id, + args: Option>, + param_names: Vec, + }, + /// Parameterized types + Generic(String), + /// Fixed-size numeric types (plus int/usize/float), char, arrays, slices, and tuples + Primitive(String), + /// `extern "ABI" fn` + FunctionPointer(Box), + /// `(String, u32, Box)` + Tuple(Vec), + /// `[u32]` + Slice(Box), + /// [u32; 15] + Array { + #[serde(rename = "type")] + type_: Box, + len: String, + }, + /// `impl TraitA + TraitB + ...` + ImplTrait(Vec), + /// `!` + Never, + /// `_` + Infer, + /// `*mut u32`, `*u8`, etc. + RawPointer { + mutable: bool, + #[serde(rename = "type")] + type_: Box, + }, + /// `&'a mut String`, `&str`, etc. + BorrowedRef { + lifetime: Option, + mutable: bool, + #[serde(rename = "type")] + type_: Box, + }, + /// `::Name` or associated types like `T::Item` where `T: Iterator` + QualifiedPath { + name: String, + self_type: Box, + #[serde(rename = "trait")] + trait_: Box, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FunctionPointer { + pub is_unsafe: bool, + pub generic_params: Vec, + pub decl: FnDecl, + pub abi: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FnDecl { + pub inputs: Vec<(String, Type)>, + pub output: Option, + pub c_variadic: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Trait { + pub is_auto: bool, + pub is_unsafe: bool, + pub items: Vec, + pub generics: Generics, + pub bounds: Vec, + pub implementors: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TraitAlias { + pub generics: Generics, + pub params: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Impl { + pub is_unsafe: bool, + pub generics: Generics, + pub provided_trait_methods: Vec, + #[serde(rename = "trait")] + pub trait_: Option, + #[serde(rename = "for")] + pub for_: Type, + pub items: Vec, + pub negative: bool, + pub synthetic: bool, + pub blanket_impl: Option, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Import { + /// The full path being imported. + pub span: String, + /// May be different from the last segment of `source` when renaming imports: + /// `use source as name;` + pub name: String, + /// The ID of the item being imported. + pub id: Option, // FIXME is this actually ever None? + /// Whether this import uses a glob: `use source::*;` + pub glob: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ProcMacro { + pub kind: MacroKind, + pub helpers: Vec, +} + +#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum MacroKind { + /// A bang macro `foo!()`. + Bang, + /// An attribute macro `#[foo]`. + Attr, + /// A derive macro `#[derive(Foo)]` + Derive, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Typedef { + #[serde(rename = "type")] + pub type_: Type, + pub generics: Generics, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OpaqueTy { + pub bounds: Vec, + pub generics: Generics, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Static { + #[serde(rename = "type")] + pub type_: Type, + pub mutable: bool, + pub expr: String, +} diff --git a/src/test/rustdoc/rustdoc-json/Makefile b/src/test/rustdoc/rustdoc-json/Makefile new file mode 100644 index 0000000000000..ad517ae95ebb6 --- /dev/null +++ b/src/test/rustdoc/rustdoc-json/Makefile @@ -0,0 +1,6 @@ +-include ../tools.mk + +tests: *.rs + $(RUSTDOC) $< -o $(TMPDIR) --output-format json + $(PYTHON) check_missing_items.py $(TMPDIR)/$(basename $<).json + $(PYTHON) compare.py $(basename $<).expected $(TMPDIR)/$(basename $<).json diff --git a/src/test/rustdoc/rustdoc-json/check_missing_items.py b/src/test/rustdoc/rustdoc-json/check_missing_items.py new file mode 100644 index 0000000000000..0004dc8fb147c --- /dev/null +++ b/src/test/rustdoc/rustdoc-json/check_missing_items.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python + +# This test ensures that every ID in the produced json actually resolves to an item either in +# `index` or `paths`. It DOES NOT check that the structure of the produced json is actually in +# any way correct, for example an empty map would pass. + +import sys +import json + +crate = json.load(open(sys.argv[1])) + + +def get_local_item(item_id): + if item_id in crate["index"]: + return crate["index"][item_id] + print("Missing local ID:", item_id) + sys.exit(1) + + +# local IDs have to be in `index`, external ones can sometimes be in `index` but otherwise have +# to be in `paths` +def valid_id(item_id): + return item_id in crate["index"] or item_id[0] != "0" and item_id in crate["paths"] + + +def check_generics(generics): + for param in generics["params"]: + check_generic_param(param) + for where_predicate in generics["where_predicates"]: + if "bound_predicate" in where_predicate: + pred = where_predicate["bound_predicate"] + check_type(pred["ty"]) + for bound in pred["bounds"]: + check_generic_bound(bound) + elif "region_predicate" in where_predicate: + pred = where_predicate["region_predicate"] + for bound in pred["bounds"]: + check_generic_bound(bound) + elif "eq_predicate" in where_predicate: + pred = where_predicate["eq_predicate"] + check_type(pred["rhs"]) + check_type(pred["lhs"]) + + +def check_generic_param(param): + if "type" in param["kind"]: + ty = param["kind"]["type"] + if ty["default"]: + check_type(ty["default"]) + for bound in ty["bounds"]: + check_generic_bound(bound) + elif "const" in param["kind"]: + check_type(param["kind"]["const"]) + + +def check_generic_bound(bound): + if "trait_bound" in bound: + for param in bound["trait_bound"]["generic_params"]: + check_generic_param(param) + check_type(bound["trait_bound"]["trait"]) + + +def check_decl(decl): + for (_name, ty) in decl["inputs"]: + check_type(ty) + if decl["output"]: + check_type(decl["output"]) + + +def check_type(ty): + if ty["kind"] == "resolved_path": + for bound in ty["inner"]["param_names"]: + check_generic_bound(bound) + args = ty["inner"]["args"] + if args: + if "angle_bracketed" in args: + for arg in args["angle_bracketed"]["args"]: + if "type" in arg: + check_type(arg["type"]) + elif "const" in arg: + check_type(arg["const"]["type"]) + for binding in args["angle_bracketed"]["bindings"]: + if "equality" in binding["binding"]: + check_type(binding["binding"]["equality"]) + elif "constraint" in binding["binding"]: + for bound in binding["binding"]["constraint"]: + check_generic_bound(bound) + elif "parenthesized" in args: + for ty in args["parenthesized"]["inputs"]: + check_type(ty) + if args["parenthesized"]["output"]: + check_type(args["parenthesized"]["output"]) + if not valid_id(ty["inner"]["id"]): + print("Type contained an invalid ID:", ty["inner"]["id"]) + sys.exit(1) + elif ty["kind"] == "tuple": + for ty in ty["inner"]: + check_type(ty) + elif ty["kind"] == "slice": + check_type(ty["inner"]) + elif ty["kind"] == "impl_trait": + for bound in ty["inner"]: + check_generic_bound(bound) + elif ty["kind"] in ("raw_pointer", "borrowed_ref", "array"): + check_type(ty["inner"]["type"]) + elif ty["kind"] == "function_pointer": + for param in ty["inner"]["generic_params"]: + check_generic_param(param) + check_decl(ty["inner"]["inner"]) + elif ty["kind"] == "qualified_path": + check_type(ty["inner"]["self_type"]) + check_type(ty["inner"]["trait"]) + + +work_list = set([crate["root"]]) +visited = work_list.copy() + +while work_list: + current = work_list.pop() + visited.add(current) + item = get_local_item(current) + # check intradoc links + for (_name, link) in item["links"].items(): + if not valid_id(link): + print("Intra-doc link contains invalid ID:", link) + + # check all fields that reference types such as generics as well as nested items + # (modules, structs, traits, and enums) + if item["kind"] == "module": + work_list |= set(item["inner"]["items"]) - visited + elif item["kind"] == "struct": + check_generics(item["inner"]["generics"]) + work_list |= (set(item["inner"]["fields"]) | set(item["inner"]["impls"])) - visited + elif item["kind"] == "struct_field": + check_type(item["inner"]) + elif item["kind"] == "enum": + check_generics(item["inner"]["generics"]) + work_list |= (set(item["inner"]["variants"]) | set(item["inner"]["impls"])) - visited + elif item["kind"] == "variant": + if item["inner"]["variant_kind"] == "tuple": + for ty in item["inner"]["variant_inner"]: + check_type(ty) + elif item["inner"]["variant_kind"] == "struct": + work_list |= set(item["inner"]["variant_inner"]) - visited + elif item["kind"] in ("function", "method"): + check_generics(item["inner"]["generics"]) + check_decl(item["inner"]["decl"]) + elif item["kind"] in ("static", "constant", "assoc_const"): + check_type(item["inner"]["type"]) + elif item["kind"] == "typedef": + check_type(item["inner"]["type"]) + check_generics(item["inner"]["generics"]) + elif item["kind"] == "opaque_ty": + check_generics(item["inner"]["generics"]) + for bound in item["inner"]["bounds"]: + check_generic_bound(bound) + elif item["kind"] == "trait_alias": + check_generics(item["inner"]["params"]) + for bound in item["inner"]["bounds"]: + check_generic_bound(bound) + elif item["kind"] == "trait": + check_generics(item["inner"]["generics"]) + for bound in item["inner"]["bounds"]: + check_generic_bound(bound) + work_list |= (set(item["inner"]["items"]) | set(item["inner"]["implementors"])) - visited + elif item["kind"] == "impl": + check_generics(item["inner"]["generics"]) + if item["inner"]["trait"]: + check_type(item["inner"]["trait"]) + if item["inner"]["blanket_impl"]: + check_type(item["inner"]["blanket_impl"]) + check_type(item["inner"]["for"]) + for assoc_item in item["inner"]["items"]: + if not valid_id(assoc_item): + print("Impl block referenced a missing ID:", assoc_item) + sys.exit(1) + elif item["kind"] == "assoc_type": + for bound in item["inner"]["bounds"]: + check_generic_bound(bound) + if item["inner"]["default"]: + check_type(item["inner"]["default"]) diff --git a/src/test/rustdoc/rustdoc-json/compare.py b/src/test/rustdoc/rustdoc-json/compare.py new file mode 100644 index 0000000000000..5daf8903e2030 --- /dev/null +++ b/src/test/rustdoc/rustdoc-json/compare.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +# This script can check that an expected json blob is a subset of what actually gets produced. +# The comparison is independent of the value of IDs (which are unstable) and instead uses their +# relative ordering to check them against eachother by looking them up in their respective blob's +# `index` or `paths` mappings. To add a new test run `rustdoc --output-format json -o . yourtest.rs` +# and then create `yourtest.expected` by stripping unnecessary details from `yourtest.json`. + +import copy +import sys +import json +import types + +# Used instead of the string ids when used as references. +# Not used as keys in `index` or `paths` +class ID(str): + pass + + +class SubsetException(Exception): + def __init__(self, msg, trace): + self.msg = msg + self.trace = msg + super().__init__("{}: {}".format(trace, msg)) + + +def check_subset(expected_main, actual_main): + expected_index = expected_main["index"] + expected_paths = expected_main["paths"] + actual_index = actual_main["index"] + actual_paths = actual_main["paths"] + already_checked = set() + + def _check_subset(expected, actual, trace): + expected_type = type(expected) + actual_type = type(actual) + if expected_type is not actual_type: + raise SubsetException( + "expected type `{}`, got `{}`".format(expected_type, actual_type), trace + ) + if expected_type in (str, int, bool) and expected != actual: + raise SubsetException("expected `{}`, got: `{}`".format(expected, actual), trace) + if expected_type is dict: + for key in expected: + if key not in actual: + raise SubsetException("Key `{}` not found in output".format(key), trace) + new_trace = copy.deepcopy(trace) + new_trace.append(key) + _check_subset(expected[key], actual[key], new_trace) + elif expected_type is list: + expected_elements = len(expected) + actual_elements = len(actual) + if expected_elements != actual_elements: + raise SubsetException( + "Found {} items, expected {}".format(expected_elements, actual_elements), trace + ) + for expected, actual in zip(expected, actual): + new_trace = copy.deepcopy(trace) + new_trace.append(expected) + _check_subset(expected, actual, new_trace) + elif expected_type is ID and expected not in already_checked: + already_checked.add(expected) + _check_subset(expected_index.get(expected, {}), actual_index.get(actual, {}), trace) + _check_subset(expected_paths.get(expected, {}), actual_paths.get(actual, {}), trace) + + _check_subset(expected_main["root"], actual_main["root"], []) + + +def rustdoc_object_hook(obj): + # No need to convert paths, index and external_crates keys to ids, since + # they are the target of resolution, and never a source itself. + if "id" in obj and obj["id"]: + obj["id"] = ID(obj["id"]) + if "root" in obj: + obj["root"] = ID(obj["root"]) + if "items" in obj: + obj["items"] = [ID(id) for id in obj["items"]] + if "variants" in obj: + obj["variants"] = [ID(id) for id in obj["variants"]] + if "fields" in obj: + obj["fields"] = [ID(id) for id in obj["fields"]] + if "impls" in obj: + obj["impls"] = [ID(id) for id in obj["impls"]] + if "implementors" in obj: + obj["implementors"] = [ID(id) for id in obj["implementors"]] + if "links" in obj: + obj["links"] = {s: ID(id) for s, id in obj["links"]} + if "variant_kind" in obj and obj["variant_kind"] == "struct": + obj["variant_inner"] = [ID(id) for id in obj["variant_inner"]] + return obj + + +def main(expected_fpath, actual_fpath): + print("checking that {} is a logical subset of {}".format(expected_fpath, actual_fpath)) + with open(expected_fpath) as expected_file: + expected_main = json.load(expected_file, object_hook=rustdoc_object_hook) + with open(actual_fpath) as actual_file: + actual_main = json.load(actual_file, object_hook=rustdoc_object_hook) + check_subset(expected_main, actual_main) + print("all checks passed") + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: `compare.py expected.json actual.json`") + else: + main(sys.argv[1], sys.argv[2]) diff --git a/src/test/rustdoc/rustdoc-json/structs.expected b/src/test/rustdoc/rustdoc-json/structs.expected new file mode 100644 index 0000000000000..45b23534bc77b --- /dev/null +++ b/src/test/rustdoc/rustdoc-json/structs.expected @@ -0,0 +1,468 @@ +{ + "root": "0:0", + "version": null, + "includes_private": false, + "index": { + "0:9": { + "crate_id": 0, + "name": "Unit", + "source": { + "filename": "structs.rs", + "begin": [ + 7, + 0 + ], + "end": [ + 7, + 16 + ] + }, + "visibility": "public", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct", + "inner": { + "struct_type": "unit", + "generics": { + "params": [], + "where_predicates": [] + }, + "fields_stripped": false, + "fields": [] + } + }, + "0:8": { + "crate_id": 0, + "name": "1", + "source": { + "filename": "structs.rs", + "begin": [ + 5, + 22 + ], + "end": [ + 5, + 28 + ] + }, + "visibility": "default", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct_field", + "inner": { + "kind": "resolved_path", + "inner": { + "name": "String", + "id": "5:5035", + "args": { + "angle_bracketed": { + "args": [], + "bindings": [] + } + }, + "param_names": [] + } + } + }, + "0:18": { + "crate_id": 0, + "name": "stuff", + "source": { + "filename": "structs.rs", + "begin": [ + 15, + 4 + ], + "end": [ + 15, + 17 + ] + }, + "visibility": "default", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct_field", + "inner": { + "kind": "resolved_path", + "inner": { + "name": "Vec", + "id": "5:4322", + "args": { + "angle_bracketed": { + "args": [ + { + "type": { + "kind": "generic", + "inner": "T" + } + } + ], + "bindings": [] + } + }, + "param_names": [] + } + } + }, + "0:11": { + "crate_id": 0, + "name": "WithPrimitives", + "source": { + "filename": "structs.rs", + "begin": [ + 9, + 0 + ], + "end": [ + 12, + 1 + ] + }, + "visibility": "public", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct", + "inner": { + "struct_type": "plain", + "generics": { + "params": [ + { + "name": "'a", + "kind": "lifetime" + } + ], + "where_predicates": [] + }, + "fields_stripped": true, + "fields": [ + "0:13", + "0:14" + ] + } + }, + "0:14": { + "crate_id": 0, + "name": "s", + "source": { + "filename": "structs.rs", + "begin": [ + 11, + 4 + ], + "end": [ + 11, + 14 + ] + }, + "visibility": "default", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct_field", + "inner": { + "kind": "borrowed_ref", + "inner": { + "lifetime": "'a", + "mutable": false, + "type": { + "kind": "primitive", + "inner": "str" + } + } + } + }, + "0:19": { + "crate_id": 0, + "name": "things", + "source": { + "filename": "structs.rs", + "begin": [ + 16, + 4 + ], + "end": [ + 16, + 25 + ] + }, + "visibility": "default", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct_field", + "inner": { + "kind": "resolved_path", + "inner": { + "name": "HashMap", + "id": "1:6600", + "args": { + "angle_bracketed": { + "args": [ + { + "type": { + "kind": "generic", + "inner": "U" + } + }, + { + "type": { + "kind": "generic", + "inner": "U" + } + } + ], + "bindings": [] + } + }, + "param_names": [] + } + } + }, + "0:15": { + "crate_id": 0, + "name": "WithGenerics", + "source": { + "filename": "structs.rs", + "begin": [ + 14, + 0 + ], + "end": [ + 17, + 1 + ] + }, + "visibility": "public", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct", + "inner": { + "struct_type": "plain", + "generics": { + "params": [ + { + "name": "T", + "kind": { + "type": { + "bounds": [], + "default": null + } + } + }, + { + "name": "U", + "kind": { + "type": { + "bounds": [], + "default": null + } + } + } + ], + "where_predicates": [] + }, + "fields_stripped": true, + "fields": [ + "0:18", + "0:19" + ] + } + }, + "0:0": { + "crate_id": 0, + "name": "structs", + "source": { + "filename": "structs.rs", + "begin": [ + 1, + 0 + ], + "end": [ + 17, + 1 + ] + }, + "visibility": "public", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "module", + "inner": { + "is_crate": true, + "items": [ + "0:4", + "0:5", + "0:9", + "0:11", + "0:15" + ] + } + }, + "0:13": { + "crate_id": 0, + "name": "num", + "source": { + "filename": "structs.rs", + "begin": [ + 10, + 4 + ], + "end": [ + 10, + 12 + ] + }, + "visibility": "default", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct_field", + "inner": { + "kind": "primitive", + "inner": "u32" + } + }, + "0:5": { + "crate_id": 0, + "name": "Tuple", + "source": { + "filename": "structs.rs", + "begin": [ + 5, + 0 + ], + "end": [ + 5, + 30 + ] + }, + "visibility": "public", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct", + "inner": { + "struct_type": "tuple", + "generics": { + "params": [], + "where_predicates": [] + }, + "fields_stripped": true, + "fields": [ + "0:7", + "0:8" + ] + } + }, + "0:4": { + "crate_id": 0, + "name": "PlainEmpty", + "source": { + "filename": "structs.rs", + "begin": [ + 3, + 0 + ], + "end": [ + 3, + 24 + ] + }, + "visibility": "public", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct", + "inner": { + "struct_type": "plain", + "generics": { + "params": [], + "where_predicates": [] + }, + "fields_stripped": false, + "fields": [] + } + }, + "0:7": { + "crate_id": 0, + "name": "0", + "source": { + "filename": "structs.rs", + "begin": [ + 5, + 17 + ], + "end": [ + 5, + 20 + ] + }, + "visibility": "default", + "docs": "", + "links": {}, + "attrs": [], + "deprecation": null, + "kind": "struct_field", + "inner": { + "kind": "primitive", + "inner": "u32" + } + } + }, + "paths": { + "5:4322": { + "crate_id": 5, + "path": [ + "alloc", + "vec", + "Vec" + ], + "kind": "struct" + }, + "5:5035": { + "crate_id": 5, + "path": [ + "alloc", + "string", + "String" + ], + "kind": "struct" + }, + "1:6600": { + "crate_id": 1, + "path": [ + "std", + "collections", + "hash", + "map", + "HashMap" + ], + "kind": "struct" + } + }, + "external_crates": { + "1": { + "name": "std" + }, + "5": { + "name": "alloc" + } + }, + "format_version": 1 +} diff --git a/src/test/rustdoc/rustdoc-json/structs.rs b/src/test/rustdoc/rustdoc-json/structs.rs new file mode 100644 index 0000000000000..43fc4743503aa --- /dev/null +++ b/src/test/rustdoc/rustdoc-json/structs.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; + +pub struct PlainEmpty {} + +pub struct Tuple(u32, String); + +pub struct Unit; + +pub struct WithPrimitives<'a> { + num: u32, + s: &'a str, +} + +pub struct WithGenerics { + stuff: Vec, + things: HashMap, +} From 1098cce27acb2d52cb3b5ddbcf28c3a06e38dc7c Mon Sep 17 00:00:00 2001 From: Nixon Enraght-Moony Date: Sun, 29 Nov 2020 16:16:25 +0000 Subject: [PATCH 2/5] Add tests for rustdoc json Move rustdoc/rustdoc-json to rustdoc-json Scaffold rustdoc-json test mode Implement run_rustdoc_json_test Fix up python Make tidy happy --- src/bootstrap/builder.rs | 1 + src/bootstrap/test.rs | 7 ++ src/test/{rustdoc => }/rustdoc-json/Makefile | 0 .../rustdoc-json/check_missing_items.py | 12 +++- .../{rustdoc => }/rustdoc-json/compare.py | 48 +++++++++---- .../rustdoc-json/structs.expected | 0 .../{rustdoc => }/rustdoc-json/structs.rs | 0 src/tools/compiletest/src/common.rs | 3 + src/tools/compiletest/src/main.rs | 2 +- src/tools/compiletest/src/runtest.rs | 67 ++++++++++++++++--- 10 files changed, 115 insertions(+), 25 deletions(-) rename src/test/{rustdoc => }/rustdoc-json/Makefile (100%) rename src/test/{rustdoc => }/rustdoc-json/check_missing_items.py (95%) rename src/test/{rustdoc => }/rustdoc-json/compare.py (69%) rename src/test/{rustdoc => }/rustdoc-json/structs.expected (100%) rename src/test/{rustdoc => }/rustdoc-json/structs.rs (100%) diff --git a/src/bootstrap/builder.rs b/src/bootstrap/builder.rs index b5bbb6372ee6c..6d97943548d5a 100644 --- a/src/bootstrap/builder.rs +++ b/src/bootstrap/builder.rs @@ -425,6 +425,7 @@ impl<'a> Builder<'a> { test::RustdocJSNotStd, test::RustdocTheme, test::RustdocUi, + test::RustdocJson, // Run bootstrap close to the end as it's unlikely to fail test::Bootstrap, // Run run-make last, since these won't pass without make on Windows diff --git a/src/bootstrap/test.rs b/src/bootstrap/test.rs index 1df50322a0732..78b5de7897d1f 100644 --- a/src/bootstrap/test.rs +++ b/src/bootstrap/test.rs @@ -904,6 +904,12 @@ host_test!(UiFullDeps { path: "src/test/ui-fulldeps", mode: "ui", suite: "ui-ful host_test!(Rustdoc { path: "src/test/rustdoc", mode: "rustdoc", suite: "rustdoc" }); host_test!(RustdocUi { path: "src/test/rustdoc-ui", mode: "ui", suite: "rustdoc-ui" }); +host_test!(RustdocJson { + path: "src/test/rustdoc-json", + mode: "rustdoc-json", + suite: "rustdoc-json" +}); + host_test!(Pretty { path: "src/test/pretty", mode: "pretty", suite: "pretty" }); default_test!(RunMake { path: "src/test/run-make", mode: "run-make", suite: "run-make" }); @@ -1001,6 +1007,7 @@ note: if you're sure you want to do this, please open an issue as to why. In the || (mode == "run-make" && suite.ends_with("fulldeps")) || (mode == "ui" && is_rustdoc) || mode == "js-doc-test" + || mode == "rustdoc-json" { cmd.arg("--rustdoc-path").arg(builder.rustdoc(compiler)); } diff --git a/src/test/rustdoc/rustdoc-json/Makefile b/src/test/rustdoc-json/Makefile similarity index 100% rename from src/test/rustdoc/rustdoc-json/Makefile rename to src/test/rustdoc-json/Makefile diff --git a/src/test/rustdoc/rustdoc-json/check_missing_items.py b/src/test/rustdoc-json/check_missing_items.py similarity index 95% rename from src/test/rustdoc/rustdoc-json/check_missing_items.py rename to src/test/rustdoc-json/check_missing_items.py index 0004dc8fb147c..3a3bf7fa3ed58 100644 --- a/src/test/rustdoc/rustdoc-json/check_missing_items.py +++ b/src/test/rustdoc-json/check_missing_items.py @@ -130,12 +130,16 @@ def check_type(ty): work_list |= set(item["inner"]["items"]) - visited elif item["kind"] == "struct": check_generics(item["inner"]["generics"]) - work_list |= (set(item["inner"]["fields"]) | set(item["inner"]["impls"])) - visited + work_list |= ( + set(item["inner"]["fields"]) | set(item["inner"]["impls"]) + ) - visited elif item["kind"] == "struct_field": check_type(item["inner"]) elif item["kind"] == "enum": check_generics(item["inner"]["generics"]) - work_list |= (set(item["inner"]["variants"]) | set(item["inner"]["impls"])) - visited + work_list |= ( + set(item["inner"]["variants"]) | set(item["inner"]["impls"]) + ) - visited elif item["kind"] == "variant": if item["inner"]["variant_kind"] == "tuple": for ty in item["inner"]["variant_inner"]: @@ -162,7 +166,9 @@ def check_type(ty): check_generics(item["inner"]["generics"]) for bound in item["inner"]["bounds"]: check_generic_bound(bound) - work_list |= (set(item["inner"]["items"]) | set(item["inner"]["implementors"])) - visited + work_list |= ( + set(item["inner"]["items"]) | set(item["inner"]["implementors"]) + ) - visited elif item["kind"] == "impl": check_generics(item["inner"]["generics"]) if item["inner"]["trait"]: diff --git a/src/test/rustdoc/rustdoc-json/compare.py b/src/test/rustdoc-json/compare.py similarity index 69% rename from src/test/rustdoc/rustdoc-json/compare.py rename to src/test/rustdoc-json/compare.py index 5daf8903e2030..afc8066685c46 100644 --- a/src/test/rustdoc/rustdoc-json/compare.py +++ b/src/test/rustdoc-json/compare.py @@ -24,7 +24,7 @@ def __init__(self, msg, trace): super().__init__("{}: {}".format(trace, msg)) -def check_subset(expected_main, actual_main): +def check_subset(expected_main, actual_main, base_dir): expected_index = expected_main["index"] expected_paths = expected_main["paths"] actual_index = actual_main["index"] @@ -39,11 +39,24 @@ def _check_subset(expected, actual, trace): "expected type `{}`, got `{}`".format(expected_type, actual_type), trace ) if expected_type in (str, int, bool) and expected != actual: - raise SubsetException("expected `{}`, got: `{}`".format(expected, actual), trace) + if expected_type == str and actual.startswith(base_dir): + if actual.replace(base_dir + "/", "") != expected: + raise SubsetException( + "expected `{}`, got: `{}`".format( + expected, actual.replace(base_dir + "/", "") + ), + trace, + ) + else: + raise SubsetException( + "expected `{}`, got: `{}`".format(expected, actual), trace + ) if expected_type is dict: for key in expected: if key not in actual: - raise SubsetException("Key `{}` not found in output".format(key), trace) + raise SubsetException( + "Key `{}` not found in output".format(key), trace + ) new_trace = copy.deepcopy(trace) new_trace.append(key) _check_subset(expected[key], actual[key], new_trace) @@ -52,7 +65,10 @@ def _check_subset(expected, actual, trace): actual_elements = len(actual) if expected_elements != actual_elements: raise SubsetException( - "Found {} items, expected {}".format(expected_elements, actual_elements), trace + "Found {} items, expected {}".format( + expected_elements, actual_elements + ), + trace, ) for expected, actual in zip(expected, actual): new_trace = copy.deepcopy(trace) @@ -60,8 +76,12 @@ def _check_subset(expected, actual, trace): _check_subset(expected, actual, new_trace) elif expected_type is ID and expected not in already_checked: already_checked.add(expected) - _check_subset(expected_index.get(expected, {}), actual_index.get(actual, {}), trace) - _check_subset(expected_paths.get(expected, {}), actual_paths.get(actual, {}), trace) + _check_subset( + expected_index.get(expected, {}), actual_index.get(actual, {}), trace + ) + _check_subset( + expected_paths.get(expected, {}), actual_paths.get(actual, {}), trace + ) _check_subset(expected_main["root"], actual_main["root"], []) @@ -90,18 +110,22 @@ def rustdoc_object_hook(obj): return obj -def main(expected_fpath, actual_fpath): - print("checking that {} is a logical subset of {}".format(expected_fpath, actual_fpath)) +def main(expected_fpath, actual_fpath, base_dir): + print( + "checking that {} is a logical subset of {}".format( + expected_fpath, actual_fpath + ) + ) with open(expected_fpath) as expected_file: expected_main = json.load(expected_file, object_hook=rustdoc_object_hook) with open(actual_fpath) as actual_file: actual_main = json.load(actual_file, object_hook=rustdoc_object_hook) - check_subset(expected_main, actual_main) + check_subset(expected_main, actual_main, base_dir) print("all checks passed") if __name__ == "__main__": - if len(sys.argv) < 3: - print("Usage: `compare.py expected.json actual.json`") + if len(sys.argv) < 4: + print("Usage: `compare.py expected.json actual.json test-dir`") else: - main(sys.argv[1], sys.argv[2]) + main(sys.argv[1], sys.argv[2], sys.argv[3]) diff --git a/src/test/rustdoc/rustdoc-json/structs.expected b/src/test/rustdoc-json/structs.expected similarity index 100% rename from src/test/rustdoc/rustdoc-json/structs.expected rename to src/test/rustdoc-json/structs.expected diff --git a/src/test/rustdoc/rustdoc-json/structs.rs b/src/test/rustdoc-json/structs.rs similarity index 100% rename from src/test/rustdoc/rustdoc-json/structs.rs rename to src/test/rustdoc-json/structs.rs diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs index 24ef98cd784d8..eba02333c8cb2 100644 --- a/src/tools/compiletest/src/common.rs +++ b/src/tools/compiletest/src/common.rs @@ -17,6 +17,7 @@ pub enum Mode { DebugInfo, Codegen, Rustdoc, + RustdocJson, CodegenUnits, Incremental, RunMake, @@ -48,6 +49,7 @@ impl FromStr for Mode { "debuginfo" => Ok(DebugInfo), "codegen" => Ok(Codegen), "rustdoc" => Ok(Rustdoc), + "rustdoc-json" => Ok(RustdocJson), "codegen-units" => Ok(CodegenUnits), "incremental" => Ok(Incremental), "run-make" => Ok(RunMake), @@ -70,6 +72,7 @@ impl fmt::Display for Mode { DebugInfo => "debuginfo", Codegen => "codegen", Rustdoc => "rustdoc", + RustdocJson => "rustdoc-json", CodegenUnits => "codegen-units", Incremental => "incremental", RunMake => "run-make", diff --git a/src/tools/compiletest/src/main.rs b/src/tools/compiletest/src/main.rs index 32347db5dbb1c..0541548aefd6d 100644 --- a/src/tools/compiletest/src/main.rs +++ b/src/tools/compiletest/src/main.rs @@ -68,7 +68,7 @@ pub fn parse_config(args: Vec) -> Config { "mode", "which sort of compile tests to run", "compile-fail | run-fail | run-pass-valgrind | pretty | debug-info | codegen | rustdoc \ - codegen-units | incremental | run-make | ui | js-doc-test | mir-opt | assembly", + | rustdoc-json | codegen-units | incremental | run-make | ui | js-doc-test | mir-opt | assembly", ) .reqopt( "", diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index 1b9f0089dced0..88cb8544e47cc 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -2,7 +2,7 @@ use crate::common::{expected_output_path, UI_EXTENSIONS, UI_FIXED, UI_STDERR, UI_STDOUT}; use crate::common::{output_base_dir, output_base_name, output_testname_unique}; -use crate::common::{Assembly, Incremental, JsDocTest, MirOpt, RunMake, Ui}; +use crate::common::{Assembly, Incremental, JsDocTest, MirOpt, RunMake, RustdocJson, Ui}; use crate::common::{Codegen, CodegenUnits, DebugInfo, Debugger, Rustdoc}; use crate::common::{CompareMode, FailMode, PassMode}; use crate::common::{CompileFail, Pretty, RunFail, RunPassValgrind}; @@ -342,6 +342,7 @@ impl<'test> TestCx<'test> { DebugInfo => self.run_debuginfo_test(), Codegen => self.run_codegen_test(), Rustdoc => self.run_rustdoc_test(), + RustdocJson => self.run_rustdoc_json_test(), CodegenUnits => self.run_codegen_units_test(), Incremental => self.run_incremental_test(), RunMake => self.run_rmake_test(), @@ -1564,7 +1565,7 @@ impl<'test> TestCx<'test> { self.compose_and_run_compiler(rustc, None) } - fn document(&self, out_dir: &Path) -> ProcRes { + fn document(&self, out_dir: &Path, json: bool) -> ProcRes { if self.props.build_aux_docs { for rel_ab in &self.props.aux_builds { let aux_testpaths = self.compute_aux_test_paths(rel_ab); @@ -1578,7 +1579,7 @@ impl<'test> TestCx<'test> { }; // Create the directory for the stdout/stderr files. create_dir_all(aux_cx.output_base_dir()).unwrap(); - let auxres = aux_cx.document(out_dir); + let auxres = aux_cx.document(out_dir, json); if !auxres.status.success() { return auxres; } @@ -1600,6 +1601,10 @@ impl<'test> TestCx<'test> { .arg(&self.testpaths.file) .args(&self.props.compile_flags); + if json { + rustdoc.arg("--output-format").arg("json"); + } + if let Some(ref linker) = self.config.linker { rustdoc.arg(format!("-Clinker={}", linker)); } @@ -1887,7 +1892,9 @@ impl<'test> TestCx<'test> { } fn is_rustdoc(&self) -> bool { - self.config.src_base.ends_with("rustdoc-ui") || self.config.src_base.ends_with("rustdoc-js") + self.config.src_base.ends_with("rustdoc-ui") + || self.config.src_base.ends_with("rustdoc-js") + || self.config.src_base.ends_with("rustdoc-json") } fn make_compile_args( @@ -1968,8 +1975,8 @@ impl<'test> TestCx<'test> { rustc.arg(dir_opt); } - RunFail | RunPassValgrind | Pretty | DebugInfo | Codegen | Rustdoc | RunMake - | CodegenUnits | JsDocTest | Assembly => { + RunFail | RunPassValgrind | Pretty | DebugInfo | Codegen | Rustdoc | RustdocJson + | RunMake | CodegenUnits | JsDocTest | Assembly => { // do not use JSON output } } @@ -2329,7 +2336,7 @@ impl<'test> TestCx<'test> { let _ = fs::remove_dir_all(&out_dir); create_dir_all(&out_dir).unwrap(); - let proc_res = self.document(&out_dir); + let proc_res = self.document(&out_dir, false); if !proc_res.status.success() { self.fatal_proc_rec("rustdoc failed!", &proc_res); } @@ -2385,7 +2392,7 @@ impl<'test> TestCx<'test> { rustc.arg("-L").arg(&new_rustdoc.aux_output_dir_name()); new_rustdoc.build_all_auxiliary(&mut rustc); - let proc_res = new_rustdoc.document(&compare_dir); + let proc_res = new_rustdoc.document(&compare_dir, false); if !proc_res.status.success() { proc_res.fatal(Some("failed to run nightly rustdoc"), || ()); } @@ -2466,6 +2473,48 @@ impl<'test> TestCx<'test> { eprintln!("{}", String::from_utf8_lossy(&output.stderr)); } + fn run_rustdoc_json_test(&self) { + //FIXME: Add bless option. + + assert!(self.revision.is_none(), "revisions not relevant here"); + + let out_dir = self.output_base_dir(); + let _ = fs::remove_dir_all(&out_dir); + create_dir_all(&out_dir).unwrap(); + + let proc_res = self.document(&out_dir, true); + if !proc_res.status.success() { + self.fatal_proc_rec("rustdoc failed!", &proc_res); + } + + let root = self.config.find_rust_src_root().unwrap(); + let mut json_out = out_dir.join(self.testpaths.file.file_stem().unwrap()); + json_out.set_extension("json"); + let res = self.cmd2procres( + Command::new(&self.config.docck_python) + .arg(root.join("src/test/rustdoc-json/check_missing_items.py")) + .arg(&json_out), + ); + + if !res.status.success() { + self.fatal_proc_rec("check_missing_items failed!", &res); + } + + let mut expected = self.testpaths.file.clone(); + expected.set_extension("expected"); + let res = self.cmd2procres( + Command::new(&self.config.docck_python) + .arg(root.join("src/test/rustdoc-json/compare.py")) + .arg(&expected) + .arg(&json_out) + .arg(&expected.parent().unwrap()), + ); + + if !res.status.success() { + self.fatal_proc_rec("compare failed!", &res); + } + } + fn get_lines>( &self, path: &P, @@ -3003,7 +3052,7 @@ impl<'test> TestCx<'test> { if let Some(nodejs) = &self.config.nodejs { let out_dir = self.output_base_dir(); - self.document(&out_dir); + self.document(&out_dir, false); let root = self.config.find_rust_src_root().unwrap(); let file_stem = From 40b5470b0db1997fe28bd774171b4f958fa7d240 Mon Sep 17 00:00:00 2001 From: Nixon Enraght-Moony Date: Mon, 30 Nov 2020 20:24:48 +0000 Subject: [PATCH 3/5] Address review comments. Go back to CRATE_DEF_INDEX Minor niceness improvements Don't output hidden items Remove striped items from fields Add $TEST_BASE_DIR Small catch --- src/librustdoc/clean/mod.rs | 2 +- src/librustdoc/json/conversions.rs | 76 +++++++++++++------------- src/librustdoc/json/mod.rs | 18 +++--- src/librustdoc/json/types.rs | 3 - src/librustdoc/lib.rs | 1 + src/test/rustdoc-json/Makefile | 6 -- src/test/rustdoc-json/compare.py | 21 +++---- src/test/rustdoc-json/structs.expected | 42 +++++--------- src/tools/compiletest/src/runtest.rs | 14 ++--- 9 files changed, 80 insertions(+), 103 deletions(-) delete mode 100644 src/test/rustdoc-json/Makefile diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index 9d42705da82a5..d294d8f02a80f 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2281,7 +2281,7 @@ impl Clean> for doctree::Import<'_> { name: None, attrs: self.attrs.clean(cx), source: self.span.clean(cx), - def_id: cx.tcx.hir().local_def_id(self.id).to_def_id(), + def_id: DefId::local(CRATE_DEF_INDEX), visibility: self.vis.clean(cx), stability: None, const_stability: None, diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 80192623d54fe..4d57b33787963 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -12,43 +12,44 @@ use crate::doctree; use crate::formats::item_type::ItemType; use crate::json::types::*; -impl From for Item { +impl From for Option { fn from(item: clean::Item) -> Self { let item_type = ItemType::from(&item); let clean::Item { source, name, attrs, - kind: inner, + kind, visibility, def_id, stability: _, deprecation, } = item; - Item { - id: def_id.into(), - crate_id: def_id.krate.as_u32(), - name, - stripped: match inner { - clean::StrippedItem(_) => true, - _ => false, - }, - source: source.into(), - visibility: visibility.into(), - docs: attrs.collapsed_doc_value().unwrap_or_default(), - links: attrs - .links - .into_iter() - .filter_map(|clean::ItemLink { link, did, .. }| did.map(|did| (link, did.into()))) - .collect(), - attrs: attrs - .other_attrs - .iter() - .map(rustc_ast_pretty::pprust::attribute_to_string) - .collect(), - deprecation: deprecation.map(Into::into), - kind: item_type.into(), - inner: inner.into(), + match kind { + clean::StrippedItem(_) => None, + _ => Some(Item { + id: def_id.into(), + crate_id: def_id.krate.as_u32(), + name, + source: source.into(), + visibility: visibility.into(), + docs: attrs.collapsed_doc_value().unwrap_or_default(), + links: attrs + .links + .into_iter() + .filter_map(|clean::ItemLink { link, did, .. }| { + did.map(|did| (link, did.into())) + }) + .collect(), + attrs: attrs + .other_attrs + .iter() + .map(rustc_ast_pretty::pprust::attribute_to_string) + .collect(), + deprecation: deprecation.map(Into::into), + kind: item_type.into(), + inner: kind.into(), + }), } } } @@ -194,10 +195,7 @@ impl From for ItemEnum { impl From for Module { fn from(module: clean::Module) -> Self { - Module { - is_crate: module.is_crate, - items: module.items.into_iter().map(|i| i.def_id.into()).collect(), - } + Module { is_crate: module.is_crate, items: ids(module.items) } } } @@ -208,7 +206,7 @@ impl From for Struct { struct_type: struct_type.into(), generics: generics.into(), fields_stripped, - fields: fields.into_iter().map(|i| i.def_id.into()).collect(), + fields: ids(fields), impls: Vec::new(), // Added in JsonRenderer::item } } @@ -221,7 +219,7 @@ impl From for Struct { struct_type: struct_type.into(), generics: generics.into(), fields_stripped, - fields: fields.into_iter().map(|i| i.def_id.into()).collect(), + fields: ids(fields), impls: Vec::new(), // Added in JsonRenderer::item } } @@ -407,7 +405,7 @@ impl From for Trait { Trait { is_auto, is_unsafe: unsafety == rustc_hir::Unsafety::Unsafe, - items: items.into_iter().map(|i| i.def_id.into()).collect(), + items: ids(items), generics: generics.into(), bounds: bounds.into_iter().map(Into::into).collect(), implementors: Vec::new(), // Added in JsonRenderer::item @@ -434,7 +432,7 @@ impl From for Impl { provided_trait_methods: provided_trait_methods.into_iter().collect(), trait_: trait_.map(Into::into), for_: for_.into(), - items: items.into_iter().map(|i| i.def_id.into()).collect(), + items: ids(items), negative: polarity == Some(clean::ImplPolarity::Negative), synthetic, blanket_impl: blanket_impl.map(Into::into), @@ -460,7 +458,7 @@ impl From for Enum { Enum { generics: generics.into(), variants_stripped, - variants: variants.into_iter().map(|i| i.def_id.into()).collect(), + variants: ids(variants), impls: Vec::new(), // Added in JsonRenderer::item } } @@ -473,7 +471,7 @@ impl From for Struct { struct_type: struct_type.into(), generics: Default::default(), fields_stripped, - fields: fields.into_iter().map(|i| i.def_id.into()).collect(), + fields: ids(fields), impls: Vec::new(), } } @@ -485,7 +483,7 @@ impl From for Variant { match variant.kind { CLike => Variant::Plain, Tuple(t) => Variant::Tuple(t.into_iter().map(Into::into).collect()), - Struct(s) => Variant::Struct(s.fields.into_iter().map(|i| i.def_id.into()).collect()), + Struct(s) => Variant::Struct(ids(s.fields)), } } } @@ -594,3 +592,7 @@ impl From for ItemKind { } } } + +fn ids(items: impl IntoIterator) -> Vec { + items.into_iter().filter(|x| !x.is_stripped()).map(|i| i.def_id.into()).collect() +} diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index 20a8de144adb3..c080ad21c0f33 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -99,7 +99,6 @@ impl JsonRenderer { .0 .last() .map(Clone::clone), - stripped: false, visibility: types::Visibility::Public, kind: types::ItemKind::Trait, inner: types::ItemEnum::TraitItem(trait_item.clone().into()), @@ -144,16 +143,17 @@ impl FormatRenderer for JsonRenderer { item.kind.inner_items().for_each(|i| self.item(i.clone(), cache).unwrap()); let id = item.def_id; - let mut new_item: types::Item = item.into(); - if let types::ItemEnum::TraitItem(ref mut t) = new_item.inner { - t.implementors = self.get_trait_implementors(id, cache) - } else if let types::ItemEnum::StructItem(ref mut s) = new_item.inner { - s.impls = self.get_impls(id, cache) - } else if let types::ItemEnum::EnumItem(ref mut e) = new_item.inner { - e.impls = self.get_impls(id, cache) + if let Some(mut new_item) = item.into(): Option { + if let types::ItemEnum::TraitItem(ref mut t) = new_item.inner { + t.implementors = self.get_trait_implementors(id, cache) + } else if let types::ItemEnum::StructItem(ref mut s) = new_item.inner { + s.impls = self.get_impls(id, cache) + } else if let types::ItemEnum::EnumItem(ref mut e) = new_item.inner { + e.impls = self.get_impls(id, cache) + } + self.index.borrow_mut().insert(id.into(), new_item); } - self.index.borrow_mut().insert(id.into(), new_item); Ok(()) } diff --git a/src/librustdoc/json/types.rs b/src/librustdoc/json/types.rs index 62705affafce1..10bf2a2acc5b9 100644 --- a/src/librustdoc/json/types.rs +++ b/src/librustdoc/json/types.rs @@ -62,9 +62,6 @@ pub struct Item { pub crate_id: u32, /// Some items such as impls don't have names. pub name: Option, - /// Whether this item is meant to be omitted from the generated documentation due to `#doc(hidden)`, - /// because it is private, or because it was inlined. - pub stripped: bool, /// The source location of this item (absent if it came from a macro expansion or inline /// assembly). pub source: Option, diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 751f230105392..80a9c3811cf66 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -14,6 +14,7 @@ #![feature(crate_visibility_modifier)] #![feature(never_type)] #![feature(once_cell)] +#![feature(type_ascription)] #![recursion_limit = "256"] #[macro_use] diff --git a/src/test/rustdoc-json/Makefile b/src/test/rustdoc-json/Makefile deleted file mode 100644 index ad517ae95ebb6..0000000000000 --- a/src/test/rustdoc-json/Makefile +++ /dev/null @@ -1,6 +0,0 @@ --include ../tools.mk - -tests: *.rs - $(RUSTDOC) $< -o $(TMPDIR) --output-format json - $(PYTHON) check_missing_items.py $(TMPDIR)/$(basename $<).json - $(PYTHON) compare.py $(basename $<).expected $(TMPDIR)/$(basename $<).json diff --git a/src/test/rustdoc-json/compare.py b/src/test/rustdoc-json/compare.py index afc8066685c46..422e48ea39a01 100644 --- a/src/test/rustdoc-json/compare.py +++ b/src/test/rustdoc-json/compare.py @@ -34,23 +34,18 @@ def check_subset(expected_main, actual_main, base_dir): def _check_subset(expected, actual, trace): expected_type = type(expected) actual_type = type(actual) + + if actual_type is str: + actual = actual.replace(base_dir, "$TEST_BASE_DIR") + if expected_type is not actual_type: raise SubsetException( "expected type `{}`, got `{}`".format(expected_type, actual_type), trace ) - if expected_type in (str, int, bool) and expected != actual: - if expected_type == str and actual.startswith(base_dir): - if actual.replace(base_dir + "/", "") != expected: - raise SubsetException( - "expected `{}`, got: `{}`".format( - expected, actual.replace(base_dir + "/", "") - ), - trace, - ) - else: - raise SubsetException( - "expected `{}`, got: `{}`".format(expected, actual), trace - ) + + + if expected_type in (int, bool, str) and expected != actual: + raise SubsetException("expected `{}`, got: `{}`".format(expected, actual), trace) if expected_type is dict: for key in expected: if key not in actual: diff --git a/src/test/rustdoc-json/structs.expected b/src/test/rustdoc-json/structs.expected index 45b23534bc77b..799829de3fd6c 100644 --- a/src/test/rustdoc-json/structs.expected +++ b/src/test/rustdoc-json/structs.expected @@ -7,7 +7,7 @@ "crate_id": 0, "name": "Unit", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 7, 0 @@ -37,7 +37,7 @@ "crate_id": 0, "name": "1", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 5, 22 @@ -72,7 +72,7 @@ "crate_id": 0, "name": "stuff", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 15, 4 @@ -114,7 +114,7 @@ "crate_id": 0, "name": "WithPrimitives", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 9, 0 @@ -141,18 +141,14 @@ ], "where_predicates": [] }, - "fields_stripped": true, - "fields": [ - "0:13", - "0:14" - ] + "fields_stripped": true } }, "0:14": { "crate_id": 0, "name": "s", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 11, 4 @@ -184,7 +180,7 @@ "crate_id": 0, "name": "things", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 16, 4 @@ -232,7 +228,7 @@ "crate_id": 0, "name": "WithGenerics", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 14, 0 @@ -273,18 +269,14 @@ ], "where_predicates": [] }, - "fields_stripped": true, - "fields": [ - "0:18", - "0:19" - ] + "fields_stripped": true } }, "0:0": { "crate_id": 0, "name": "structs", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 1, 0 @@ -315,7 +307,7 @@ "crate_id": 0, "name": "num", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 10, 4 @@ -340,7 +332,7 @@ "crate_id": 0, "name": "Tuple", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 5, 0 @@ -362,18 +354,14 @@ "params": [], "where_predicates": [] }, - "fields_stripped": true, - "fields": [ - "0:7", - "0:8" - ] + "fields_stripped": true } }, "0:4": { "crate_id": 0, "name": "PlainEmpty", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 3, 0 @@ -403,7 +391,7 @@ "crate_id": 0, "name": "0", "source": { - "filename": "structs.rs", + "filename": "$TEST_BASE_DIR/structs.rs", "begin": [ 5, 17 diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index 88cb8544e47cc..e9089b4b15baa 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -1565,7 +1565,7 @@ impl<'test> TestCx<'test> { self.compose_and_run_compiler(rustc, None) } - fn document(&self, out_dir: &Path, json: bool) -> ProcRes { + fn document(&self, out_dir: &Path) -> ProcRes { if self.props.build_aux_docs { for rel_ab in &self.props.aux_builds { let aux_testpaths = self.compute_aux_test_paths(rel_ab); @@ -1579,7 +1579,7 @@ impl<'test> TestCx<'test> { }; // Create the directory for the stdout/stderr files. create_dir_all(aux_cx.output_base_dir()).unwrap(); - let auxres = aux_cx.document(out_dir, json); + let auxres = aux_cx.document(out_dir); if !auxres.status.success() { return auxres; } @@ -1601,7 +1601,7 @@ impl<'test> TestCx<'test> { .arg(&self.testpaths.file) .args(&self.props.compile_flags); - if json { + if self.config.mode == RustdocJson { rustdoc.arg("--output-format").arg("json"); } @@ -2336,7 +2336,7 @@ impl<'test> TestCx<'test> { let _ = fs::remove_dir_all(&out_dir); create_dir_all(&out_dir).unwrap(); - let proc_res = self.document(&out_dir, false); + let proc_res = self.document(&out_dir); if !proc_res.status.success() { self.fatal_proc_rec("rustdoc failed!", &proc_res); } @@ -2392,7 +2392,7 @@ impl<'test> TestCx<'test> { rustc.arg("-L").arg(&new_rustdoc.aux_output_dir_name()); new_rustdoc.build_all_auxiliary(&mut rustc); - let proc_res = new_rustdoc.document(&compare_dir, false); + let proc_res = new_rustdoc.document(&compare_dir); if !proc_res.status.success() { proc_res.fatal(Some("failed to run nightly rustdoc"), || ()); } @@ -2482,7 +2482,7 @@ impl<'test> TestCx<'test> { let _ = fs::remove_dir_all(&out_dir); create_dir_all(&out_dir).unwrap(); - let proc_res = self.document(&out_dir, true); + let proc_res = self.document(&out_dir); if !proc_res.status.success() { self.fatal_proc_rec("rustdoc failed!", &proc_res); } @@ -3052,7 +3052,7 @@ impl<'test> TestCx<'test> { if let Some(nodejs) = &self.config.nodejs { let out_dir = self.output_base_dir(); - self.document(&out_dir, false); + self.document(&out_dir); let root = self.config.find_rust_src_root().unwrap(); let file_stem = From 601820028c01b80b0a5368e6dcd019ef34711943 Mon Sep 17 00:00:00 2001 From: Nixon Enraght-Moony Date: Tue, 1 Dec 2020 18:43:24 +0000 Subject: [PATCH 4/5] Discard `const_stability` --- src/librustdoc/json/conversions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 4d57b33787963..afb8dfa676657 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -23,6 +23,7 @@ impl From for Option { visibility, def_id, stability: _, + const_stability: _, deprecation, } = item; match kind { From d619271f2bf90a4fce327ef868920ef39ee22a4c Mon Sep 17 00:00:00 2001 From: Nixon Enraght-Moony Date: Wed, 2 Dec 2020 20:41:20 +0000 Subject: [PATCH 5/5] Normalize windows path seperators. --- src/test/rustdoc-json/compare.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/test/rustdoc-json/compare.py b/src/test/rustdoc-json/compare.py index 422e48ea39a01..b0c5b16a19700 100644 --- a/src/test/rustdoc-json/compare.py +++ b/src/test/rustdoc-json/compare.py @@ -4,7 +4,8 @@ # The comparison is independent of the value of IDs (which are unstable) and instead uses their # relative ordering to check them against eachother by looking them up in their respective blob's # `index` or `paths` mappings. To add a new test run `rustdoc --output-format json -o . yourtest.rs` -# and then create `yourtest.expected` by stripping unnecessary details from `yourtest.json`. +# and then create `yourtest.expected` by stripping unnecessary details from `yourtest.json`. If +# you're on windows, replace `\` with `/`. import copy import sys @@ -36,7 +37,7 @@ def _check_subset(expected, actual, trace): actual_type = type(actual) if actual_type is str: - actual = actual.replace(base_dir, "$TEST_BASE_DIR") + actual = normalize(actual).replace(base_dir, "$TEST_BASE_DIR") if expected_type is not actual_type: raise SubsetException( @@ -118,9 +119,11 @@ def main(expected_fpath, actual_fpath, base_dir): check_subset(expected_main, actual_main, base_dir) print("all checks passed") +def normalize(s): + return s.replace('\\', '/') if __name__ == "__main__": if len(sys.argv) < 4: print("Usage: `compare.py expected.json actual.json test-dir`") else: - main(sys.argv[1], sys.argv[2], sys.argv[3]) + main(sys.argv[1], sys.argv[2], normalize(sys.argv[3]))