Auto merge of #139054 - matthiaskrgr:rollup-2bk2fb4, r=matthiaskrgr
Rollup of 7 pull requests Successful merges: - #137889 (update outdated doc with new example) - #138104 (Greatly simplify doctest parsing and information extraction) - #138678 (rustc_resolve: fix instability in lib.rmeta contents) - #138986 (feat(config): Add ChangeId enum for suppressing warnings) - #139038 (Update target maintainers for thumb targets to reflect new REWG Arm team name) - #139045 (bootstrap: update `test_find` test) - #139047 (Remove ScopeDepth) Failed merges: - #139044 (bootstrap: Avoid cloning `change-id` list) r? `@ghost` `@rustbot` modify labels: rollup
This commit is contained in:
commit
19f42cb9bb
30 changed files with 443 additions and 485 deletions
|
@ -4300,6 +4300,7 @@ name = "rustc_resolve"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"itertools",
|
||||
"pulldown-cmark 0.11.3",
|
||||
"rustc_arena",
|
||||
"rustc_ast",
|
||||
|
|
|
@ -28,8 +28,9 @@
|
|||
# - A new option
|
||||
# - A change in the default values
|
||||
#
|
||||
# If `change-id` does not match the version that is currently running,
|
||||
# `x.py` will inform you about the changes made on bootstrap.
|
||||
# If the change-id does not match the version currently in use, x.py will
|
||||
# display the changes made to the bootstrap.
|
||||
# To suppress these warnings, you can set change-id = "ignore".
|
||||
#change-id = <latest change id in src/bootstrap/src/utils/change_tracker.rs>
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
@ -23,18 +23,11 @@ use tracing::debug;
|
|||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct Context {
|
||||
/// The scope that contains any new variables declared, plus its depth in
|
||||
/// the scope tree.
|
||||
/// The scope that contains any new variables declared.
|
||||
var_parent: Option<Scope>,
|
||||
|
||||
/// Region parent of expressions, etc., plus its depth in the scope tree.
|
||||
parent: Option<(Scope, ScopeDepth)>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
fn set_var_parent(&mut self) {
|
||||
self.var_parent = self.parent.map(|(p, _)| p);
|
||||
}
|
||||
/// Region parent of expressions, etc.
|
||||
parent: Option<Scope>,
|
||||
}
|
||||
|
||||
struct ScopeResolutionVisitor<'tcx> {
|
||||
|
@ -119,7 +112,7 @@ fn resolve_block<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, blk: &'tcx hi
|
|||
// itself has returned.
|
||||
|
||||
visitor.enter_node_scope_with_dtor(blk.hir_id.local_id);
|
||||
visitor.cx.set_var_parent();
|
||||
visitor.cx.var_parent = visitor.cx.parent;
|
||||
|
||||
{
|
||||
// This block should be kept approximately in sync with
|
||||
|
@ -138,7 +131,7 @@ fn resolve_block<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, blk: &'tcx hi
|
|||
local_id: blk.hir_id.local_id,
|
||||
data: ScopeData::Remainder(FirstStatementIndex::new(i)),
|
||||
});
|
||||
visitor.cx.set_var_parent();
|
||||
visitor.cx.var_parent = visitor.cx.parent;
|
||||
visitor.visit_stmt(statement);
|
||||
// We need to back out temporarily to the last enclosing scope
|
||||
// for the `else` block, so that even the temporaries receiving
|
||||
|
@ -163,7 +156,7 @@ fn resolve_block<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, blk: &'tcx hi
|
|||
local_id: blk.hir_id.local_id,
|
||||
data: ScopeData::Remainder(FirstStatementIndex::new(i)),
|
||||
});
|
||||
visitor.cx.set_var_parent();
|
||||
visitor.cx.var_parent = visitor.cx.parent;
|
||||
visitor.visit_stmt(statement)
|
||||
}
|
||||
hir::StmtKind::Item(..) => {
|
||||
|
@ -213,7 +206,7 @@ fn resolve_arm<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, arm: &'tcx hir:
|
|||
visitor.terminating_scopes.insert(arm.hir_id.local_id);
|
||||
|
||||
visitor.enter_node_scope_with_dtor(arm.hir_id.local_id);
|
||||
visitor.cx.set_var_parent();
|
||||
visitor.cx.var_parent = visitor.cx.parent;
|
||||
|
||||
if let Some(expr) = arm.guard
|
||||
&& !has_let_expr(expr)
|
||||
|
@ -490,7 +483,7 @@ fn resolve_expr<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, expr: &'tcx hi
|
|||
ScopeData::IfThen
|
||||
};
|
||||
visitor.enter_scope(Scope { local_id: then.hir_id.local_id, data });
|
||||
visitor.cx.set_var_parent();
|
||||
visitor.cx.var_parent = visitor.cx.parent;
|
||||
visitor.visit_expr(cond);
|
||||
visitor.visit_expr(then);
|
||||
visitor.cx = expr_cx;
|
||||
|
@ -505,7 +498,7 @@ fn resolve_expr<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, expr: &'tcx hi
|
|||
ScopeData::IfThen
|
||||
};
|
||||
visitor.enter_scope(Scope { local_id: then.hir_id.local_id, data });
|
||||
visitor.cx.set_var_parent();
|
||||
visitor.cx.var_parent = visitor.cx.parent;
|
||||
visitor.visit_expr(cond);
|
||||
visitor.visit_expr(then);
|
||||
visitor.cx = expr_cx;
|
||||
|
@ -545,7 +538,7 @@ fn resolve_expr<'tcx>(visitor: &mut ScopeResolutionVisitor<'tcx>, expr: &'tcx hi
|
|||
// Keep traversing up while we can.
|
||||
match visitor.scope_tree.parent_map.get(&scope) {
|
||||
// Don't cross from closure bodies to their parent.
|
||||
Some(&(superscope, _)) => match superscope.data {
|
||||
Some(&superscope) => match superscope.data {
|
||||
ScopeData::CallSite => break,
|
||||
_ => scope = superscope,
|
||||
},
|
||||
|
@ -781,20 +774,16 @@ fn resolve_local<'tcx>(
|
|||
|
||||
impl<'tcx> ScopeResolutionVisitor<'tcx> {
|
||||
/// Records the current parent (if any) as the parent of `child_scope`.
|
||||
/// Returns the depth of `child_scope`.
|
||||
fn record_child_scope(&mut self, child_scope: Scope) -> ScopeDepth {
|
||||
fn record_child_scope(&mut self, child_scope: Scope) {
|
||||
let parent = self.cx.parent;
|
||||
self.scope_tree.record_scope_parent(child_scope, parent);
|
||||
// If `child_scope` has no parent, it must be the root node, and so has
|
||||
// a depth of 1. Otherwise, its depth is one more than its parent's.
|
||||
parent.map_or(1, |(_p, d)| d + 1)
|
||||
}
|
||||
|
||||
/// Records the current parent (if any) as the parent of `child_scope`,
|
||||
/// and sets `child_scope` as the new current parent.
|
||||
fn enter_scope(&mut self, child_scope: Scope) {
|
||||
let child_depth = self.record_child_scope(child_scope);
|
||||
self.cx.parent = Some((child_scope, child_depth));
|
||||
self.record_child_scope(child_scope);
|
||||
self.cx.parent = Some(child_scope);
|
||||
}
|
||||
|
||||
fn enter_node_scope_with_dtor(&mut self, id: hir::ItemLocalId) {
|
||||
|
@ -855,7 +844,7 @@ impl<'tcx> Visitor<'tcx> for ScopeResolutionVisitor<'tcx> {
|
|||
self.enter_body(body.value.hir_id, |this| {
|
||||
if this.tcx.hir_body_owner_kind(owner_id).is_fn_or_closure() {
|
||||
// The arguments and `self` are parented to the fn.
|
||||
this.cx.set_var_parent();
|
||||
this.cx.var_parent = this.cx.parent;
|
||||
for param in body.params {
|
||||
this.visit_pat(param.pat);
|
||||
}
|
||||
|
|
|
@ -188,10 +188,10 @@ fn check_well_formed(tcx: TyCtxt<'_>, def_id: LocalDefId) -> Result<(), ErrorGua
|
|||
/// definition itself. For example, this definition would be illegal:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct Ref<'a, T> { x: &'a T }
|
||||
/// struct StaticRef<T> { x: &'static T }
|
||||
/// ```
|
||||
///
|
||||
/// because the type did not declare that `T:'a`.
|
||||
/// because the type did not declare that `T: 'static`.
|
||||
///
|
||||
/// We do this check as a pre-pass before checking fn bodies because if these constraints are
|
||||
/// not included it frequently leads to confusing errors in fn bodies. So it's better to check
|
||||
|
|
|
@ -199,8 +199,6 @@ impl Scope {
|
|||
}
|
||||
}
|
||||
|
||||
pub type ScopeDepth = u32;
|
||||
|
||||
/// The region scope tree encodes information about region relationships.
|
||||
#[derive(Default, Debug, HashStable)]
|
||||
pub struct ScopeTree {
|
||||
|
@ -213,7 +211,7 @@ pub struct ScopeTree {
|
|||
/// conditional expression or repeating block. (Note that the
|
||||
/// enclosing scope ID for the block associated with a closure is
|
||||
/// the closure itself.)
|
||||
pub parent_map: FxIndexMap<Scope, (Scope, ScopeDepth)>,
|
||||
pub parent_map: FxIndexMap<Scope, Scope>,
|
||||
|
||||
/// Maps from a variable or binding ID to the block in which that
|
||||
/// variable is declared.
|
||||
|
@ -328,7 +326,7 @@ pub struct YieldData {
|
|||
}
|
||||
|
||||
impl ScopeTree {
|
||||
pub fn record_scope_parent(&mut self, child: Scope, parent: Option<(Scope, ScopeDepth)>) {
|
||||
pub fn record_scope_parent(&mut self, child: Scope, parent: Option<Scope>) {
|
||||
debug!("{:?}.parent = {:?}", child, parent);
|
||||
|
||||
if let Some(p) = parent {
|
||||
|
@ -353,7 +351,7 @@ impl ScopeTree {
|
|||
|
||||
/// Returns the narrowest scope that encloses `id`, if any.
|
||||
pub fn opt_encl_scope(&self, id: Scope) -> Option<Scope> {
|
||||
self.parent_map.get(&id).cloned().map(|(p, _)| p)
|
||||
self.parent_map.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the lifetime of the local variable `var_id`, if any.
|
||||
|
|
|
@ -38,7 +38,7 @@ impl RvalueScopes {
|
|||
let mut id = Scope { local_id: expr_id, data: ScopeData::Node };
|
||||
let mut backwards_incompatible = None;
|
||||
|
||||
while let Some(&(p, _)) = region_scope_tree.parent_map.get(&id) {
|
||||
while let Some(&p) = region_scope_tree.parent_map.get(&id) {
|
||||
match p.data {
|
||||
ScopeData::Destruction => {
|
||||
debug!("temporary_scope({expr_id:?}) = {id:?} [enclosing]");
|
||||
|
|
|
@ -6,6 +6,7 @@ edition = "2024"
|
|||
[dependencies]
|
||||
# tidy-alphabetical-start
|
||||
bitflags = "2.4.1"
|
||||
itertools = "0.12"
|
||||
pulldown-cmark = { version = "0.11", features = ["html"], default-features = false }
|
||||
rustc_arena = { path = "../rustc_arena" }
|
||||
rustc_ast = { path = "../rustc_ast" }
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use std::mem;
|
||||
use std::ops::Range;
|
||||
|
||||
use itertools::Itertools;
|
||||
use pulldown_cmark::{
|
||||
BrokenLink, BrokenLinkCallback, CowStr, Event, LinkType, Options, Parser, Tag,
|
||||
};
|
||||
use rustc_ast as ast;
|
||||
use rustc_ast::attr::AttributeExt;
|
||||
use rustc_ast::util::comments::beautify_doc_string;
|
||||
use rustc_data_structures::fx::{FxHashSet, FxIndexMap};
|
||||
use rustc_data_structures::fx::FxIndexMap;
|
||||
use rustc_data_structures::unord::UnordSet;
|
||||
use rustc_middle::ty::TyCtxt;
|
||||
use rustc_span::def_id::DefId;
|
||||
use rustc_span::{DUMMY_SP, InnerSpan, Span, Symbol, kw, sym};
|
||||
|
@ -422,7 +424,7 @@ fn parse_links<'md>(doc: &'md str) -> Vec<Box<str>> {
|
|||
);
|
||||
let mut links = Vec::new();
|
||||
|
||||
let mut refids = FxHashSet::default();
|
||||
let mut refids = UnordSet::default();
|
||||
|
||||
while let Some(event) = event_iter.next() {
|
||||
match event {
|
||||
|
@ -454,7 +456,7 @@ fn parse_links<'md>(doc: &'md str) -> Vec<Box<str>> {
|
|||
}
|
||||
}
|
||||
|
||||
for (label, refdef) in event_iter.reference_definitions().iter() {
|
||||
for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
|
||||
if !refids.contains(label) {
|
||||
links.push(preprocess_link(&refdef.dest));
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ use std::str::FromStr;
|
|||
use std::{env, process};
|
||||
|
||||
use bootstrap::{
|
||||
Build, CONFIG_CHANGE_HISTORY, Config, Flags, Subcommand, debug, find_recent_config_change_ids,
|
||||
human_readable_changes, t,
|
||||
Build, CONFIG_CHANGE_HISTORY, ChangeId, Config, Flags, Subcommand, debug,
|
||||
find_recent_config_change_ids, human_readable_changes, t,
|
||||
};
|
||||
#[cfg(feature = "tracing")]
|
||||
use tracing::instrument;
|
||||
|
@ -155,51 +155,53 @@ fn check_version(config: &Config) -> Option<String> {
|
|||
let latest_change_id = CONFIG_CHANGE_HISTORY.last().unwrap().change_id;
|
||||
let warned_id_path = config.out.join("bootstrap").join(".last-warned-change-id");
|
||||
|
||||
if let Some(mut id) = config.change_id {
|
||||
if id == latest_change_id {
|
||||
return None;
|
||||
let mut id = match config.change_id {
|
||||
Some(ChangeId::Id(id)) if id == latest_change_id => return None,
|
||||
Some(ChangeId::Ignore) => return None,
|
||||
Some(ChangeId::Id(id)) => id,
|
||||
None => {
|
||||
msg.push_str("WARNING: The `change-id` is missing in the `bootstrap.toml`. This means that you will not be able to track the major changes made to the bootstrap configurations.\n");
|
||||
msg.push_str("NOTE: to silence this warning, ");
|
||||
msg.push_str(&format!(
|
||||
"add `change-id = {latest_change_id}` or change-id = \"ignore\" at the top of `bootstrap.toml`"
|
||||
));
|
||||
return Some(msg);
|
||||
}
|
||||
|
||||
// Always try to use `change-id` from .last-warned-change-id first. If it doesn't exist,
|
||||
// then use the one from the bootstrap.toml. This way we never show the same warnings
|
||||
// more than once.
|
||||
if let Ok(t) = fs::read_to_string(&warned_id_path) {
|
||||
let last_warned_id = usize::from_str(&t)
|
||||
.unwrap_or_else(|_| panic!("{} is corrupted.", warned_id_path.display()));
|
||||
|
||||
// We only use the last_warned_id if it exists in `CONFIG_CHANGE_HISTORY`.
|
||||
// Otherwise, we may retrieve all the changes if it's not the highest value.
|
||||
// For better understanding, refer to `change_tracker::find_recent_config_change_ids`.
|
||||
if CONFIG_CHANGE_HISTORY.iter().any(|config| config.change_id == last_warned_id) {
|
||||
id = last_warned_id;
|
||||
}
|
||||
};
|
||||
|
||||
let changes = find_recent_config_change_ids(id);
|
||||
|
||||
if changes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
msg.push_str("There have been changes to x.py since you last updated:\n");
|
||||
msg.push_str(&human_readable_changes(&changes));
|
||||
|
||||
msg.push_str("NOTE: to silence this warning, ");
|
||||
msg.push_str(&format!(
|
||||
"update `bootstrap.toml` to use `change-id = {latest_change_id}` instead"
|
||||
));
|
||||
|
||||
if io::stdout().is_terminal() {
|
||||
t!(fs::write(warned_id_path, latest_change_id.to_string()));
|
||||
}
|
||||
} else {
|
||||
msg.push_str("WARNING: The `change-id` is missing in the `bootstrap.toml`. This means that you will not be able to track the major changes made to the bootstrap configurations.\n");
|
||||
msg.push_str("NOTE: to silence this warning, ");
|
||||
msg.push_str(&format!(
|
||||
"add `change-id = {latest_change_id}` at the top of `bootstrap.toml`"
|
||||
));
|
||||
};
|
||||
|
||||
// Always try to use `change-id` from .last-warned-change-id first. If it doesn't exist,
|
||||
// then use the one from the bootstrap.toml. This way we never show the same warnings
|
||||
// more than once.
|
||||
if let Ok(t) = fs::read_to_string(&warned_id_path) {
|
||||
let last_warned_id = usize::from_str(&t)
|
||||
.unwrap_or_else(|_| panic!("{} is corrupted.", warned_id_path.display()));
|
||||
|
||||
// We only use the last_warned_id if it exists in `CONFIG_CHANGE_HISTORY`.
|
||||
// Otherwise, we may retrieve all the changes if it's not the highest value.
|
||||
// For better understanding, refer to `change_tracker::find_recent_config_change_ids`.
|
||||
if CONFIG_CHANGE_HISTORY.iter().any(|config| config.change_id == last_warned_id) {
|
||||
id = last_warned_id;
|
||||
}
|
||||
};
|
||||
|
||||
let changes = find_recent_config_change_ids(id);
|
||||
|
||||
if changes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
msg.push_str("There have been changes to x.py since you last updated:\n");
|
||||
msg.push_str(&human_readable_changes(&changes));
|
||||
|
||||
msg.push_str("NOTE: to silence this warning, ");
|
||||
msg.push_str(&format!(
|
||||
"update `bootstrap.toml` to use `change-id = {latest_change_id}` or change-id = \"ignore\" instead"
|
||||
));
|
||||
|
||||
if io::stdout().is_terminal() {
|
||||
t!(fs::write(warned_id_path, latest_change_id.to_string()));
|
||||
}
|
||||
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
|
|
|
@ -193,7 +193,7 @@ pub enum GccCiMode {
|
|||
/// `bootstrap.example.toml`.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Config {
|
||||
pub change_id: Option<usize>,
|
||||
pub change_id: Option<ChangeId>,
|
||||
pub bypass_bootstrap_lock: bool,
|
||||
pub ccache: Option<String>,
|
||||
/// Call Build::ninja() instead of this.
|
||||
|
@ -700,14 +700,36 @@ pub(crate) struct TomlConfig {
|
|||
profile: Option<String>,
|
||||
}
|
||||
|
||||
/// This enum is used for deserializing change IDs from TOML, allowing both numeric values and the string `"ignore"`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ChangeId {
|
||||
Ignore,
|
||||
Id(usize),
|
||||
}
|
||||
|
||||
/// Since we use `#[serde(deny_unknown_fields)]` on `TomlConfig`, we need a wrapper type
|
||||
/// for the "change-id" field to parse it even if other fields are invalid. This ensures
|
||||
/// that if deserialization fails due to other fields, we can still provide the changelogs
|
||||
/// to allow developers to potentially find the reason for the failure in the logs..
|
||||
#[derive(Deserialize, Default)]
|
||||
pub(crate) struct ChangeIdWrapper {
|
||||
#[serde(alias = "change-id")]
|
||||
pub(crate) inner: Option<usize>,
|
||||
#[serde(alias = "change-id", default, deserialize_with = "deserialize_change_id")]
|
||||
pub(crate) inner: Option<ChangeId>,
|
||||
}
|
||||
|
||||
fn deserialize_change_id<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<ChangeId>, D::Error> {
|
||||
let value = toml::Value::deserialize(deserializer)?;
|
||||
Ok(match value {
|
||||
toml::Value::String(s) if s == "ignore" => Some(ChangeId::Ignore),
|
||||
toml::Value::Integer(i) => Some(ChangeId::Id(i as usize)),
|
||||
_ => {
|
||||
return Err(serde::de::Error::custom(
|
||||
"expected \"ignore\" or an integer for change-id",
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Describes how to handle conflicts in merging two [`TomlConfig`]
|
||||
|
@ -1351,10 +1373,11 @@ impl Config {
|
|||
toml::from_str(&contents)
|
||||
.and_then(|table: toml::Value| TomlConfig::deserialize(table))
|
||||
.inspect_err(|_| {
|
||||
if let Ok(Some(changes)) = toml::from_str(&contents)
|
||||
.and_then(|table: toml::Value| ChangeIdWrapper::deserialize(table))
|
||||
.map(|change_id| change_id.inner.map(crate::find_recent_config_change_ids))
|
||||
if let Ok(ChangeIdWrapper { inner: Some(ChangeId::Id(id)) }) =
|
||||
toml::from_str::<toml::Value>(&contents)
|
||||
.and_then(|table: toml::Value| ChangeIdWrapper::deserialize(table))
|
||||
{
|
||||
let changes = crate::find_recent_config_change_ids(id);
|
||||
if !changes.is_empty() {
|
||||
println!(
|
||||
"WARNING: There have been changes to x.py since you last updated:\n{}",
|
||||
|
|
|
@ -10,6 +10,7 @@ use serde::Deserialize;
|
|||
|
||||
use super::flags::Flags;
|
||||
use super::{ChangeIdWrapper, Config, RUSTC_IF_UNCHANGED_ALLOWED_PATHS};
|
||||
use crate::ChangeId;
|
||||
use crate::core::build_steps::clippy::{LintConfig, get_clippy_rules_in_order};
|
||||
use crate::core::build_steps::llvm;
|
||||
use crate::core::build_steps::llvm::LLVM_INVALIDATION_PATHS;
|
||||
|
@ -151,7 +152,7 @@ runner = "x86_64-runner"
|
|||
)
|
||||
},
|
||||
);
|
||||
assert_eq!(config.change_id, Some(1), "setting top-level value");
|
||||
assert_eq!(config.change_id, Some(ChangeId::Id(1)), "setting top-level value");
|
||||
assert_eq!(
|
||||
config.rust_lto,
|
||||
crate::core::config::RustcLto::Fat,
|
||||
|
@ -291,7 +292,7 @@ fn parse_change_id_with_unknown_field() {
|
|||
"#;
|
||||
|
||||
let change_id_wrapper: ChangeIdWrapper = toml::from_str(config).unwrap();
|
||||
assert_eq!(change_id_wrapper.inner, Some(3461));
|
||||
assert_eq!(change_id_wrapper.inner, Some(ChangeId::Id(3461)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -45,8 +45,8 @@ mod core;
|
|||
mod utils;
|
||||
|
||||
pub use core::builder::PathSet;
|
||||
pub use core::config::Config;
|
||||
pub use core::config::flags::{Flags, Subcommand};
|
||||
pub use core::config::{ChangeId, Config};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use tracing::{instrument, span};
|
||||
|
|
|
@ -264,7 +264,7 @@ fn test_find_target_without_config() {
|
|||
fn test_find() {
|
||||
let mut build = Build::new(Config { ..Config::parse(Flags::parse(&["check".to_owned()])) });
|
||||
let target1 = TargetSelection::from_user("x86_64-unknown-linux-gnu");
|
||||
let target2 = TargetSelection::from_user("arm-linux-androideabi");
|
||||
let target2 = TargetSelection::from_user("x86_64-unknown-openbsd");
|
||||
build.targets.push(target1.clone());
|
||||
build.hosts.push(target2.clone());
|
||||
find(&build);
|
||||
|
|
|
@ -390,4 +390,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
|
|||
severity: ChangeSeverity::Warning,
|
||||
summary: "The default configuration filename has changed from `config.toml` to `bootstrap.toml`. `config.toml` is deprecated but remains supported for backward compatibility.",
|
||||
},
|
||||
ChangeInfo {
|
||||
change_id: 138986,
|
||||
severity: ChangeSeverity::Info,
|
||||
summary: "You can now use `change-id = \"ignore\"` to suppress `change-id ` warnings in the console.",
|
||||
},
|
||||
];
|
||||
|
|
|
@ -26,8 +26,7 @@ only option because there is no FPU support in [Armv6-M].
|
|||
|
||||
## Target maintainers
|
||||
|
||||
* [Rust Embedded Devices Working Group Cortex-M
|
||||
Team](https://github.com/rust-embedded), `cortex-m@teams.rust-embedded.org`
|
||||
* [Rust Embedded Devices Working Group Arm Team](https://github.com/rust-embedded/wg?tab=readme-ov-file#the-arm-team)
|
||||
|
||||
## Target CPU and Target Feature options
|
||||
|
||||
|
|
|
@ -21,8 +21,7 @@ See [`arm-none-eabi`](arm-none-eabi.md) for information applicable to all
|
|||
|
||||
## Target maintainers
|
||||
|
||||
* [Rust Embedded Devices Working Group Cortex-M
|
||||
Team](https://github.com/rust-embedded), `cortex-m@teams.rust-embedded.org`
|
||||
* [Rust Embedded Devices Working Group Arm Team](https://github.com/rust-embedded/wg?tab=readme-ov-file#the-arm-team)
|
||||
|
||||
## Target CPU and Target Feature options
|
||||
|
||||
|
|
|
@ -22,8 +22,7 @@ only option because there is no FPU support in [Armv7-M].
|
|||
|
||||
## Target maintainers
|
||||
|
||||
* [Rust Embedded Devices Working Group Cortex-M
|
||||
Team](https://github.com/rust-embedded), `cortex-m@teams.rust-embedded.org`
|
||||
* [Rust Embedded Devices Working Group Arm Team](https://github.com/rust-embedded/wg?tab=readme-ov-file#the-arm-team)
|
||||
|
||||
## Target CPU and Target Feature options
|
||||
|
||||
|
|
|
@ -22,8 +22,7 @@ only option because there is no FPU support in [Armv8-M] Baseline.
|
|||
|
||||
## Target maintainers
|
||||
|
||||
* [Rust Embedded Devices Working Group Cortex-M
|
||||
Team](https://github.com/rust-embedded), `cortex-m@teams.rust-embedded.org`
|
||||
* [Rust Embedded Devices Working Group Arm Team](https://github.com/rust-embedded/wg?tab=readme-ov-file#the-arm-team)
|
||||
|
||||
## Target CPU and Target Feature options
|
||||
|
||||
|
|
|
@ -25,8 +25,7 @@ See [`arm-none-eabi`](arm-none-eabi.md) for information applicable to all
|
|||
|
||||
## Target maintainers
|
||||
|
||||
* [Rust Embedded Devices Working Group Cortex-M
|
||||
Team](https://github.com/rust-embedded), `cortex-m@teams.rust-embedded.org`
|
||||
* [Rust Embedded Devices Working Group Arm Team](https://github.com/rust-embedded/wg?tab=readme-ov-file#the-arm-team)
|
||||
|
||||
## Target CPU and Target Feature options
|
||||
|
||||
|
|
|
@ -5,22 +5,36 @@ use std::fmt::{self, Write as _};
|
|||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_ast as ast;
|
||||
use rustc_ast::token::{Delimiter, TokenKind};
|
||||
use rustc_ast::tokenstream::TokenTree;
|
||||
use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
|
||||
use rustc_errors::ColorConfig;
|
||||
use rustc_errors::emitter::stderr_destination;
|
||||
use rustc_errors::{ColorConfig, FatalError};
|
||||
use rustc_parse::new_parser_from_source_str;
|
||||
use rustc_parse::parser::attr::InnerAttrPolicy;
|
||||
use rustc_session::parse::ParseSess;
|
||||
use rustc_span::FileName;
|
||||
use rustc_span::edition::Edition;
|
||||
use rustc_span::source_map::SourceMap;
|
||||
use rustc_span::symbol::sym;
|
||||
use rustc_span::{FileName, kw};
|
||||
use tracing::debug;
|
||||
|
||||
use super::GlobalTestOptions;
|
||||
use crate::display::Joined as _;
|
||||
use crate::html::markdown::LangString;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ParseSourceInfo {
|
||||
has_main_fn: bool,
|
||||
already_has_extern_crate: bool,
|
||||
supports_color: bool,
|
||||
has_global_allocator: bool,
|
||||
has_macro_def: bool,
|
||||
everything_else: String,
|
||||
crates: String,
|
||||
crate_attrs: String,
|
||||
maybe_crate_attrs: String,
|
||||
}
|
||||
|
||||
/// This struct contains information about the doctest itself which is then used to generate
|
||||
/// doctest source code appropriately.
|
||||
pub(crate) struct DocTestBuilder {
|
||||
|
@ -34,7 +48,7 @@ pub(crate) struct DocTestBuilder {
|
|||
pub(crate) crates: String,
|
||||
pub(crate) everything_else: String,
|
||||
pub(crate) test_id: Option<String>,
|
||||
pub(crate) failed_ast: bool,
|
||||
pub(crate) invalid_ast: bool,
|
||||
pub(crate) can_be_merged: bool,
|
||||
}
|
||||
|
||||
|
@ -53,9 +67,26 @@ impl DocTestBuilder {
|
|||
!lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
|
||||
});
|
||||
|
||||
let Some(SourceInfo { crate_attrs, maybe_crate_attrs, crates, everything_else }) =
|
||||
partition_source(source, edition)
|
||||
let result = rustc_driver::catch_fatal_errors(|| {
|
||||
rustc_span::create_session_if_not_set_then(edition, |_| {
|
||||
parse_source(source, &crate_name)
|
||||
})
|
||||
});
|
||||
|
||||
let Ok(Ok(ParseSourceInfo {
|
||||
has_main_fn,
|
||||
already_has_extern_crate,
|
||||
supports_color,
|
||||
has_global_allocator,
|
||||
has_macro_def,
|
||||
everything_else,
|
||||
crates,
|
||||
crate_attrs,
|
||||
maybe_crate_attrs,
|
||||
})) = result
|
||||
else {
|
||||
// If the AST returned an error, we don't want this doctest to be merged with the
|
||||
// others.
|
||||
return Self::invalid(
|
||||
String::new(),
|
||||
String::new(),
|
||||
|
@ -65,35 +96,12 @@ impl DocTestBuilder {
|
|||
);
|
||||
};
|
||||
|
||||
// Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
|
||||
// crate already is included.
|
||||
let Ok((
|
||||
ParseSourceInfo {
|
||||
has_main_fn,
|
||||
found_extern_crate,
|
||||
supports_color,
|
||||
has_global_allocator,
|
||||
has_macro_def,
|
||||
..
|
||||
},
|
||||
failed_ast,
|
||||
)) = check_for_main_and_extern_crate(
|
||||
crate_name,
|
||||
source,
|
||||
&everything_else,
|
||||
&crates,
|
||||
edition,
|
||||
can_merge_doctests,
|
||||
)
|
||||
else {
|
||||
// If the parser panicked due to a fatal error, pass the test code through unchanged.
|
||||
// The error will be reported during compilation.
|
||||
return Self::invalid(crate_attrs, maybe_crate_attrs, crates, everything_else, test_id);
|
||||
};
|
||||
// If the AST returned an error, we don't want this doctest to be merged with the
|
||||
// others. Same if it contains `#[feature]` or `#[no_std]`.
|
||||
debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
|
||||
debug!("crates:\n{crates}");
|
||||
debug!("after:\n{everything_else}");
|
||||
|
||||
// If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either.
|
||||
let can_be_merged = can_merge_doctests
|
||||
&& !failed_ast
|
||||
&& !has_global_allocator
|
||||
&& crate_attrs.is_empty()
|
||||
// If this is a merged doctest and a defined macro uses `$crate`, then the path will
|
||||
|
@ -106,9 +114,9 @@ impl DocTestBuilder {
|
|||
maybe_crate_attrs,
|
||||
crates,
|
||||
everything_else,
|
||||
already_has_extern_crate: found_extern_crate,
|
||||
already_has_extern_crate,
|
||||
test_id,
|
||||
failed_ast: false,
|
||||
invalid_ast: false,
|
||||
can_be_merged,
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +137,7 @@ impl DocTestBuilder {
|
|||
everything_else,
|
||||
already_has_extern_crate: false,
|
||||
test_id,
|
||||
failed_ast: true,
|
||||
invalid_ast: true,
|
||||
can_be_merged: false,
|
||||
}
|
||||
}
|
||||
|
@ -143,9 +151,10 @@ impl DocTestBuilder {
|
|||
opts: &GlobalTestOptions,
|
||||
crate_name: Option<&str>,
|
||||
) -> (String, usize) {
|
||||
if self.failed_ast {
|
||||
if self.invalid_ast {
|
||||
// If the AST failed to compile, no need to go generate a complete doctest, the error
|
||||
// will be better this way.
|
||||
debug!("invalid AST:\n{test_code}");
|
||||
return (test_code.to_string(), 0);
|
||||
}
|
||||
let mut line_offset = 0;
|
||||
|
@ -168,9 +177,24 @@ impl DocTestBuilder {
|
|||
|
||||
// Now push any outer attributes from the example, assuming they
|
||||
// are intended to be crate attributes.
|
||||
prog.push_str(&self.crate_attrs);
|
||||
prog.push_str(&self.maybe_crate_attrs);
|
||||
prog.push_str(&self.crates);
|
||||
if !self.crate_attrs.is_empty() {
|
||||
prog.push_str(&self.crate_attrs);
|
||||
if !self.crate_attrs.ends_with('\n') {
|
||||
prog.push('\n');
|
||||
}
|
||||
}
|
||||
if !self.maybe_crate_attrs.is_empty() {
|
||||
prog.push_str(&self.maybe_crate_attrs);
|
||||
if !self.maybe_crate_attrs.ends_with('\n') {
|
||||
prog.push('\n');
|
||||
}
|
||||
}
|
||||
if !self.crates.is_empty() {
|
||||
prog.push_str(&self.crates);
|
||||
if !self.crates.ends_with('\n') {
|
||||
prog.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Don't inject `extern crate std` because it's already injected by the
|
||||
// compiler.
|
||||
|
@ -255,14 +279,7 @@ impl DocTestBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
enum ParsingResult {
|
||||
Failed,
|
||||
AstError,
|
||||
Ok,
|
||||
}
|
||||
|
||||
fn cancel_error_count(psess: &ParseSess) {
|
||||
fn reset_error_count(psess: &ParseSess) {
|
||||
// Reset errors so that they won't be reported as compiler bugs when dropping the
|
||||
// dcx. Any errors in the tests will be reported when the test file is compiled,
|
||||
// Note that we still need to cancel the errors above otherwise `Diag` will panic on
|
||||
|
@ -270,17 +287,19 @@ fn cancel_error_count(psess: &ParseSess) {
|
|||
psess.dcx().reset_err_count();
|
||||
}
|
||||
|
||||
fn parse_source(
|
||||
source: String,
|
||||
info: &mut ParseSourceInfo,
|
||||
crate_name: &Option<&str>,
|
||||
) -> ParsingResult {
|
||||
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
|
||||
|
||||
fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceInfo, ()> {
|
||||
use rustc_errors::DiagCtxt;
|
||||
use rustc_errors::emitter::{Emitter, HumanEmitter};
|
||||
use rustc_parse::parser::ForceCollect;
|
||||
use rustc_span::source_map::FilePathMapping;
|
||||
|
||||
let filename = FileName::anon_source_code(&source);
|
||||
let mut info =
|
||||
ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
|
||||
|
||||
let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
|
||||
|
||||
let filename = FileName::anon_source_code(&wrapped_source);
|
||||
|
||||
// Any errors in parsing should also appear when the doctest is compiled for real, so just
|
||||
// send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
|
||||
|
@ -299,346 +318,163 @@ fn parse_source(
|
|||
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
|
||||
let psess = ParseSess::with_dcx(dcx, sm);
|
||||
|
||||
let mut parser = match new_parser_from_source_str(&psess, filename, source) {
|
||||
let mut parser = match new_parser_from_source_str(&psess, filename, wrapped_source) {
|
||||
Ok(p) => p,
|
||||
Err(errs) => {
|
||||
errs.into_iter().for_each(|err| err.cancel());
|
||||
cancel_error_count(&psess);
|
||||
return ParsingResult::Failed;
|
||||
reset_error_count(&psess);
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
let mut parsing_result = ParsingResult::Ok;
|
||||
|
||||
fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
|
||||
let extra_len = DOCTEST_CODE_WRAPPER.len();
|
||||
// We need to shift by the length of `DOCTEST_CODE_WRAPPER` because we
|
||||
// added it at the beginning of the source we provided to the parser.
|
||||
let mut hi = span.hi().0 as usize - extra_len;
|
||||
if hi > source.len() {
|
||||
hi = source.len();
|
||||
}
|
||||
s.push_str(&source[*prev_span_hi..hi]);
|
||||
*prev_span_hi = hi;
|
||||
}
|
||||
|
||||
// Recurse through functions body. It is necessary because the doctest source code is
|
||||
// wrapped in a function to limit the number of AST errors. If we don't recurse into
|
||||
// functions, we would thing all top-level items (so basically nothing).
|
||||
fn check_item(
|
||||
item: &ast::Item,
|
||||
info: &mut ParseSourceInfo,
|
||||
crate_name: &Option<&str>,
|
||||
is_top_level: bool,
|
||||
) {
|
||||
fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
|
||||
let mut is_extern_crate = false;
|
||||
if !info.has_global_allocator
|
||||
&& item.attrs.iter().any(|attr| attr.name_or_empty() == sym::global_allocator)
|
||||
{
|
||||
info.has_global_allocator = true;
|
||||
}
|
||||
match item.kind {
|
||||
ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
|
||||
if item.ident.name == sym::main && is_top_level {
|
||||
ast::ItemKind::Fn(_) if !info.has_main_fn => {
|
||||
// We only push if it's the top item because otherwise, we would duplicate
|
||||
// its content since the top-level item was already added.
|
||||
if item.ident.name == sym::main {
|
||||
info.has_main_fn = true;
|
||||
}
|
||||
if let Some(ref body) = fn_item.body {
|
||||
for stmt in &body.stmts {
|
||||
match stmt.kind {
|
||||
ast::StmtKind::Item(ref item) => {
|
||||
check_item(item, info, crate_name, false)
|
||||
}
|
||||
ast::StmtKind::MacCall(..) => info.found_macro = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::ItemKind::ExternCrate(original) => {
|
||||
if !info.found_extern_crate
|
||||
is_extern_crate = true;
|
||||
if !info.already_has_extern_crate
|
||||
&& let Some(crate_name) = crate_name
|
||||
{
|
||||
info.found_extern_crate = match original {
|
||||
info.already_has_extern_crate = match original {
|
||||
Some(name) => name.as_str() == *crate_name,
|
||||
None => item.ident.as_str() == *crate_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
ast::ItemKind::MacCall(..) => info.found_macro = true,
|
||||
ast::ItemKind::MacroDef(..) => info.has_macro_def = true,
|
||||
ast::ItemKind::MacroDef(..) => {
|
||||
info.has_macro_def = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
is_extern_crate
|
||||
}
|
||||
|
||||
loop {
|
||||
match parser.parse_item(ForceCollect::No) {
|
||||
Ok(Some(item)) => {
|
||||
check_item(&item, info, crate_name, true);
|
||||
let mut prev_span_hi = 0;
|
||||
let not_crate_attrs = [sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
|
||||
let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
|
||||
|
||||
if info.has_main_fn && info.found_extern_crate {
|
||||
break;
|
||||
let result = match parsed {
|
||||
Ok(Some(ref item))
|
||||
if let ast::ItemKind::Fn(ref fn_item) = item.kind
|
||||
&& let Some(ref body) = fn_item.body =>
|
||||
{
|
||||
for attr in &item.attrs {
|
||||
let attr_name = attr.name_or_empty();
|
||||
|
||||
if attr.style == AttrStyle::Outer || not_crate_attrs.contains(&attr_name) {
|
||||
// There is one exception to these attributes:
|
||||
// `#![allow(internal_features)]`. If this attribute is used, we need to
|
||||
// consider it only as a crate-level attribute.
|
||||
if attr_name == sym::allow
|
||||
&& let Some(list) = attr.meta_item_list()
|
||||
&& list.iter().any(|sub_attr| {
|
||||
sub_attr.name_or_empty().as_str() == "internal_features"
|
||||
})
|
||||
{
|
||||
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
|
||||
} else {
|
||||
push_to_s(
|
||||
&mut info.maybe_crate_attrs,
|
||||
source,
|
||||
attr.span,
|
||||
&mut prev_span_hi,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
parsing_result = ParsingResult::AstError;
|
||||
e.cancel();
|
||||
break;
|
||||
for stmt in &body.stmts {
|
||||
let mut is_extern_crate = false;
|
||||
match stmt.kind {
|
||||
StmtKind::Item(ref item) => {
|
||||
is_extern_crate = check_item(&item, &mut info, crate_name);
|
||||
}
|
||||
StmtKind::Expr(ref expr) if matches!(expr.kind, ast::ExprKind::Err(_)) => {
|
||||
reset_error_count(&psess);
|
||||
return Err(());
|
||||
}
|
||||
StmtKind::MacCall(ref mac_call) if !info.has_main_fn => {
|
||||
let mut iter = mac_call.mac.args.tokens.iter();
|
||||
|
||||
while let Some(token) = iter.next() {
|
||||
if let TokenTree::Token(token, _) = token
|
||||
&& let TokenKind::Ident(name, _) = token.kind
|
||||
&& name == kw::Fn
|
||||
&& let Some(TokenTree::Token(fn_token, _)) = iter.peek()
|
||||
&& let TokenKind::Ident(fn_name, _) = fn_token.kind
|
||||
&& fn_name == sym::main
|
||||
&& let Some(TokenTree::Delimited(_, _, Delimiter::Parenthesis, _)) = {
|
||||
iter.next();
|
||||
iter.peek()
|
||||
}
|
||||
{
|
||||
info.has_main_fn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
|
||||
// tweak the span to include the attributes as well.
|
||||
let mut span = stmt.span;
|
||||
if let Some(attr) =
|
||||
stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
|
||||
{
|
||||
span = span.with_lo(attr.span.lo());
|
||||
}
|
||||
if info.everything_else.is_empty()
|
||||
&& (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
|
||||
{
|
||||
// To keep the doctest code "as close as possible" to the original, we insert
|
||||
// all the code located between this new span and the previous span which
|
||||
// might contain code comments and backlines.
|
||||
push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
|
||||
}
|
||||
if !is_extern_crate {
|
||||
push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
|
||||
} else {
|
||||
push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
// The supplied item is only used for diagnostics,
|
||||
// which are swallowed here anyway.
|
||||
parser.maybe_consume_incorrect_semicolon(None);
|
||||
}
|
||||
|
||||
cancel_error_count(&psess);
|
||||
parsing_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ParseSourceInfo {
|
||||
has_main_fn: bool,
|
||||
found_extern_crate: bool,
|
||||
found_macro: bool,
|
||||
supports_color: bool,
|
||||
has_global_allocator: bool,
|
||||
has_macro_def: bool,
|
||||
}
|
||||
|
||||
fn check_for_main_and_extern_crate(
|
||||
crate_name: Option<&str>,
|
||||
original_source_code: &str,
|
||||
everything_else: &str,
|
||||
crates: &str,
|
||||
edition: Edition,
|
||||
can_merge_doctests: bool,
|
||||
) -> Result<(ParseSourceInfo, bool), FatalError> {
|
||||
let result = rustc_driver::catch_fatal_errors(|| {
|
||||
rustc_span::create_session_if_not_set_then(edition, |_| {
|
||||
let mut info =
|
||||
ParseSourceInfo { found_extern_crate: crate_name.is_none(), ..Default::default() };
|
||||
|
||||
let mut parsing_result =
|
||||
parse_source(format!("{crates}{everything_else}"), &mut info, &crate_name);
|
||||
// No need to double-check this if the "merged doctests" feature isn't enabled (so
|
||||
// before the 2024 edition).
|
||||
if can_merge_doctests && parsing_result != ParsingResult::Ok {
|
||||
// If we found an AST error, we want to ensure it's because of an expression being
|
||||
// used outside of a function.
|
||||
//
|
||||
// To do so, we wrap in a function in order to make sure that the doctest AST is
|
||||
// correct. For example, if your doctest is `foo::bar()`, if we don't wrap it in a
|
||||
// block, it would emit an AST error, which would be problematic for us since we
|
||||
// want to filter out such errors which aren't "real" errors.
|
||||
//
|
||||
// The end goal is to be able to merge as many doctests as possible as one for much
|
||||
// faster doctests run time.
|
||||
parsing_result = parse_source(
|
||||
format!("{crates}\nfn __doctest_wrap(){{{everything_else}\n}}"),
|
||||
&mut info,
|
||||
&crate_name,
|
||||
);
|
||||
}
|
||||
|
||||
(info, parsing_result)
|
||||
})
|
||||
});
|
||||
let (mut info, parsing_result) = match result {
|
||||
Err(..) | Ok((_, ParsingResult::Failed)) => return Err(FatalError),
|
||||
Ok((info, parsing_result)) => (info, parsing_result),
|
||||
Err(e) => {
|
||||
e.cancel();
|
||||
Err(())
|
||||
}
|
||||
_ => Err(()),
|
||||
};
|
||||
|
||||
// If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
|
||||
// see it. In that case, run the old text-based scan to see if they at least have a main
|
||||
// function written inside a macro invocation. See
|
||||
// https://github.com/rust-lang/rust/issues/56898
|
||||
if info.found_macro
|
||||
&& !info.has_main_fn
|
||||
&& original_source_code
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let comment = line.find("//");
|
||||
if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line }
|
||||
})
|
||||
.any(|code| code.contains("fn main"))
|
||||
{
|
||||
info.has_main_fn = true;
|
||||
}
|
||||
|
||||
Ok((info, parsing_result != ParsingResult::Ok))
|
||||
}
|
||||
|
||||
enum AttrKind {
|
||||
CrateAttr,
|
||||
Attr,
|
||||
}
|
||||
|
||||
/// Returns `Some` if the attribute is complete and `Some(true)` if it is an attribute that can be
|
||||
/// placed at the crate root.
|
||||
fn check_if_attr_is_complete(source: &str, edition: Edition) -> Option<AttrKind> {
|
||||
if source.is_empty() {
|
||||
// Empty content so nothing to check in here...
|
||||
return None;
|
||||
}
|
||||
let not_crate_attrs = [sym::forbid, sym::allow, sym::warn, sym::deny];
|
||||
|
||||
rustc_driver::catch_fatal_errors(|| {
|
||||
rustc_span::create_session_if_not_set_then(edition, |_| {
|
||||
use rustc_errors::DiagCtxt;
|
||||
use rustc_errors::emitter::HumanEmitter;
|
||||
use rustc_span::source_map::FilePathMapping;
|
||||
|
||||
let filename = FileName::anon_source_code(source);
|
||||
// Any errors in parsing should also appear when the doctest is compiled for real, so just
|
||||
// send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
|
||||
let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
|
||||
let fallback_bundle = rustc_errors::fallback_fluent_bundle(
|
||||
rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
|
||||
false,
|
||||
);
|
||||
|
||||
let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
|
||||
|
||||
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
|
||||
let psess = ParseSess::with_dcx(dcx, sm);
|
||||
let mut parser = match new_parser_from_source_str(&psess, filename, source.to_owned()) {
|
||||
Ok(p) => p,
|
||||
Err(errs) => {
|
||||
errs.into_iter().for_each(|err| err.cancel());
|
||||
// If there is an unclosed delimiter, an error will be returned by the
|
||||
// tokentrees.
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// If a parsing error happened, it's very likely that the attribute is incomplete.
|
||||
let ret = match parser.parse_attribute(InnerAttrPolicy::Permitted) {
|
||||
Ok(attr) => {
|
||||
let attr_name = attr.name_or_empty();
|
||||
|
||||
if not_crate_attrs.contains(&attr_name) {
|
||||
// There is one exception to these attributes:
|
||||
// `#![allow(internal_features)]`. If this attribute is used, we need to
|
||||
// consider it only as a crate-level attribute.
|
||||
if attr_name == sym::allow
|
||||
&& let Some(list) = attr.meta_item_list()
|
||||
&& list.iter().any(|sub_attr| {
|
||||
sub_attr.name_or_empty().as_str() == "internal_features"
|
||||
})
|
||||
{
|
||||
Some(AttrKind::CrateAttr)
|
||||
} else {
|
||||
Some(AttrKind::Attr)
|
||||
}
|
||||
} else {
|
||||
Some(AttrKind::CrateAttr)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
e.cancel();
|
||||
None
|
||||
}
|
||||
};
|
||||
ret
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
fn handle_attr(mod_attr_pending: &mut String, source_info: &mut SourceInfo, edition: Edition) {
|
||||
if let Some(attr_kind) = check_if_attr_is_complete(mod_attr_pending, edition) {
|
||||
let push_to = match attr_kind {
|
||||
AttrKind::CrateAttr => &mut source_info.crate_attrs,
|
||||
AttrKind::Attr => &mut source_info.maybe_crate_attrs,
|
||||
};
|
||||
push_to.push_str(mod_attr_pending);
|
||||
push_to.push('\n');
|
||||
// If it's complete, then we can clear the pending content.
|
||||
mod_attr_pending.clear();
|
||||
} else {
|
||||
mod_attr_pending.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SourceInfo {
|
||||
crate_attrs: String,
|
||||
maybe_crate_attrs: String,
|
||||
crates: String,
|
||||
everything_else: String,
|
||||
}
|
||||
|
||||
fn partition_source(s: &str, edition: Edition) -> Option<SourceInfo> {
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
enum PartitionState {
|
||||
Attrs,
|
||||
Crates,
|
||||
Other,
|
||||
}
|
||||
let mut source_info = SourceInfo::default();
|
||||
let mut state = PartitionState::Attrs;
|
||||
let mut mod_attr_pending = String::new();
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
|
||||
// FIXME(misdreavus): if a doc comment is placed on an extern crate statement, it will be
|
||||
// shunted into "everything else"
|
||||
match state {
|
||||
PartitionState::Attrs => {
|
||||
state = if trimline.starts_with("#![") {
|
||||
mod_attr_pending = line.to_owned();
|
||||
handle_attr(&mut mod_attr_pending, &mut source_info, edition);
|
||||
continue;
|
||||
} else if trimline.chars().all(|c| c.is_whitespace())
|
||||
|| (trimline.starts_with("//") && !trimline.starts_with("///"))
|
||||
{
|
||||
PartitionState::Attrs
|
||||
} else if trimline.starts_with("extern crate")
|
||||
|| trimline.starts_with("#[macro_use] extern crate")
|
||||
{
|
||||
PartitionState::Crates
|
||||
} else {
|
||||
// First we check if the previous attribute was "complete"...
|
||||
if !mod_attr_pending.is_empty() {
|
||||
// If not, then we append the new line into the pending attribute to check
|
||||
// if this time it's complete...
|
||||
mod_attr_pending.push_str(line);
|
||||
if !trimline.is_empty() {
|
||||
handle_attr(&mut mod_attr_pending, &mut source_info, edition);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
PartitionState::Other
|
||||
}
|
||||
};
|
||||
}
|
||||
PartitionState::Crates => {
|
||||
state = if trimline.starts_with("extern crate")
|
||||
|| trimline.starts_with("#[macro_use] extern crate")
|
||||
|| trimline.chars().all(|c| c.is_whitespace())
|
||||
|| (trimline.starts_with("//") && !trimline.starts_with("///"))
|
||||
{
|
||||
PartitionState::Crates
|
||||
} else {
|
||||
PartitionState::Other
|
||||
};
|
||||
}
|
||||
PartitionState::Other => {}
|
||||
}
|
||||
|
||||
match state {
|
||||
PartitionState::Attrs => {
|
||||
source_info.crate_attrs.push_str(line);
|
||||
source_info.crate_attrs.push('\n');
|
||||
}
|
||||
PartitionState::Crates => {
|
||||
source_info.crates.push_str(line);
|
||||
source_info.crates.push('\n');
|
||||
}
|
||||
PartitionState::Other => {
|
||||
source_info.everything_else.push_str(line);
|
||||
source_info.everything_else.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mod_attr_pending.is_empty() {
|
||||
debug!("invalid doctest code: {s:?}");
|
||||
return None;
|
||||
}
|
||||
|
||||
source_info.everything_else = source_info.everything_else.trim().to_string();
|
||||
|
||||
debug!("crate_attrs:\n{}{}", source_info.crate_attrs, source_info.maybe_crate_attrs);
|
||||
debug!("crates:\n{}", source_info.crates);
|
||||
debug!("after:\n{}", source_info.everything_else);
|
||||
|
||||
Some(source_info)
|
||||
reset_error_count(&psess);
|
||||
result
|
||||
}
|
||||
|
|
|
@ -197,6 +197,7 @@ fn make_test_crate_attrs() {
|
|||
assert_eq!(2+2, 4);";
|
||||
let expected = "#![allow(unused)]
|
||||
#![feature(sick_rad)]
|
||||
|
||||
fn main() {
|
||||
assert_eq!(2+2, 4);
|
||||
}"
|
||||
|
@ -228,8 +229,8 @@ fn make_test_fake_main() {
|
|||
let input = "//Ceci n'est pas une `fn main`
|
||||
assert_eq!(2+2, 4);";
|
||||
let expected = "#![allow(unused)]
|
||||
//Ceci n'est pas une `fn main`
|
||||
fn main() {
|
||||
//Ceci n'est pas une `fn main`
|
||||
assert_eq!(2+2, 4);
|
||||
}"
|
||||
.to_string();
|
||||
|
@ -259,8 +260,8 @@ fn make_test_issues_21299() {
|
|||
assert_eq!(2+2, 4);";
|
||||
|
||||
let expected = "#![allow(unused)]
|
||||
// fn main
|
||||
fn main() {
|
||||
// fn main
|
||||
assert_eq!(2+2, 4);
|
||||
}"
|
||||
.to_string();
|
||||
|
@ -401,3 +402,76 @@ fn check_split_args() {
|
|||
compare("a\n\t \rb", &["a", "b"]);
|
||||
compare("a\n\t1 \rb", &["a", "1", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_in_attrs() {
|
||||
// If there is an inline code comment after attributes, we need to ensure that
|
||||
// a backline will be added to prevent generating code "inside" it (and thus generating)
|
||||
// invalid code.
|
||||
let opts = default_global_opts("");
|
||||
let input = "\
|
||||
#![feature(rustdoc_internals)]
|
||||
#![allow(internal_features)]
|
||||
#![doc(rust_logo)]
|
||||
//! This crate has the Rust(tm) branding on it.";
|
||||
let expected = "\
|
||||
#![allow(unused)]
|
||||
#![feature(rustdoc_internals)]
|
||||
#![allow(internal_features)]
|
||||
#![doc(rust_logo)]
|
||||
//! This crate has the Rust(tm) branding on it.
|
||||
fn main() {
|
||||
|
||||
}"
|
||||
.to_string();
|
||||
let (output, len) = make_test(input, None, false, &opts, None);
|
||||
assert_eq!((output, len), (expected, 2));
|
||||
|
||||
// And same, if there is a `main` function provided by the user, we ensure that it's
|
||||
// correctly separated.
|
||||
let input = "\
|
||||
#![feature(rustdoc_internals)]
|
||||
#![allow(internal_features)]
|
||||
#![doc(rust_logo)]
|
||||
//! This crate has the Rust(tm) branding on it.
|
||||
fn main() {}";
|
||||
let expected = "\
|
||||
#![allow(unused)]
|
||||
#![feature(rustdoc_internals)]
|
||||
#![allow(internal_features)]
|
||||
#![doc(rust_logo)]
|
||||
//! This crate has the Rust(tm) branding on it.
|
||||
|
||||
fn main() {}"
|
||||
.to_string();
|
||||
let (output, len) = make_test(input, None, false, &opts, None);
|
||||
assert_eq!((output, len), (expected, 1));
|
||||
}
|
||||
|
||||
// This test ensures that the only attributes taken into account when we switch between
|
||||
// "crate level" content and the rest doesn't include inner attributes span, as it would
|
||||
// include part of the item and generate broken code.
|
||||
#[test]
|
||||
fn inner_attributes() {
|
||||
let opts = default_global_opts("");
|
||||
let input = r#"
|
||||
//! A doc comment that applies to the implicit anonymous module of this crate
|
||||
|
||||
pub mod outer_module {
|
||||
//!! - Still an inner line doc (but with a bang at the beginning)
|
||||
}
|
||||
"#;
|
||||
let expected = "#![allow(unused)]
|
||||
|
||||
//! A doc comment that applies to the implicit anonymous module of this crate
|
||||
|
||||
|
||||
fn main() {
|
||||
pub mod outer_module {
|
||||
//!! - Still an inner line doc (but with a bang at the beginning)
|
||||
}
|
||||
}"
|
||||
.to_string();
|
||||
let (output, len) = make_test(input, None, false, &opts, None);
|
||||
assert_eq!((output, len), (expected, 2));
|
||||
}
|
||||
|
|
|
@ -1724,6 +1724,7 @@ pub(crate) fn markdown_links<'md, R>(
|
|||
md: &'md str,
|
||||
preprocess_link: impl Fn(MarkdownLink) -> Option<R>,
|
||||
) -> Vec<R> {
|
||||
use itertools::Itertools;
|
||||
if md.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
@ -1882,7 +1883,7 @@ pub(crate) fn markdown_links<'md, R>(
|
|||
let mut links = Vec::new();
|
||||
|
||||
let mut refdefs = FxIndexMap::default();
|
||||
for (label, refdef) in event_iter.reference_definitions().iter() {
|
||||
for (label, refdef) in event_iter.reference_definitions().iter().sorted_by_key(|x| x.0) {
|
||||
refdefs.insert(label.to_string(), (false, refdef.dest.to_string(), refdef.span.clone()));
|
||||
}
|
||||
|
||||
|
|
|
@ -58,21 +58,21 @@ $DIR/doctest.rs:
|
|||
LL| |//!
|
||||
LL| |//! doctest with custom main:
|
||||
LL| |//! ```
|
||||
LL| 1|//! fn some_func() {
|
||||
LL| 1|//! println!("called some_func()");
|
||||
LL| 1|//! }
|
||||
LL| |//!
|
||||
LL| |//! #[derive(Debug)]
|
||||
LL| |//! struct SomeError;
|
||||
LL| |//! fn some_func() {
|
||||
LL| |//! println!("called some_func()");
|
||||
LL| |//! }
|
||||
LL| 1|//!
|
||||
LL| 1|//! #[derive(Debug)]
|
||||
LL| 1|//! struct SomeError;
|
||||
LL| |//!
|
||||
LL| |//! extern crate doctest_crate;
|
||||
LL| |//!
|
||||
LL| 1|//! fn doctest_main() -> Result<(), SomeError> {
|
||||
LL| |//! fn doctest_main() -> Result<(), SomeError> {
|
||||
LL| 1|//! some_func();
|
||||
LL| 1|//! doctest_crate::fn_run_in_doctests(2);
|
||||
LL| 1|//! Ok(())
|
||||
LL| 1|//! }
|
||||
LL| |//!
|
||||
LL| 1|//!
|
||||
LL| |//! // this `main` is not shown as covered, as it clashes with all the other
|
||||
LL| |//! // `main` functions that were automatically generated for doctests
|
||||
LL| |//! fn main() -> Result<(), SomeError> {
|
||||
|
|
|
@ -8,11 +8,11 @@ fn main() {
|
|||
|
||||
let should_contain = &[
|
||||
"input.rs - foo (line 5)",
|
||||
"input.rs:7:15",
|
||||
"input.rs:8:15",
|
||||
"input.rs - bar (line 13)",
|
||||
"input.rs:15:15",
|
||||
"input.rs:16:15",
|
||||
"input.rs - bar (line 22)",
|
||||
"input.rs:24:15",
|
||||
"input.rs:25:15",
|
||||
];
|
||||
for text in should_contain {
|
||||
assert!(output.contains(text), "output doesn't contains {:?}", text);
|
||||
|
|
|
@ -6,7 +6,7 @@ successes:
|
|||
|
||||
---- $DIR/display-output.rs - foo (line 9) stdout ----
|
||||
warning: unused variable: `x`
|
||||
--> $DIR/display-output.rs:11:5
|
||||
--> $DIR/display-output.rs:12:5
|
||||
|
|
||||
LL | let x = 12;
|
||||
| ^ help: if this is intentional, prefix it with an underscore: `_x`
|
||||
|
@ -19,13 +19,13 @@ LL | #![warn(unused)]
|
|||
= note: `#[warn(unused_variables)]` implied by `#[warn(unused)]`
|
||||
|
||||
warning: unused variable: `x`
|
||||
--> $DIR/display-output.rs:13:8
|
||||
--> $DIR/display-output.rs:14:8
|
||||
|
|
||||
LL | fn foo(x: &dyn std::fmt::Display) {}
|
||||
| ^ help: if this is intentional, prefix it with an underscore: `_x`
|
||||
|
||||
warning: function `foo` is never used
|
||||
--> $DIR/display-output.rs:13:4
|
||||
--> $DIR/display-output.rs:14:4
|
||||
|
|
||||
LL | fn foo(x: &dyn std::fmt::Display) {}
|
||||
| ^^^
|
||||
|
|
23
tests/rustdoc-ui/doctest/extern-crate.rs
Normal file
23
tests/rustdoc-ui/doctest/extern-crate.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
//@ check-pass
|
||||
//@ compile-flags:--test --test-args=--test-threads=1
|
||||
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
|
||||
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
|
||||
|
||||
// This test ensures that crate imports are placed outside of the `main` function
|
||||
// so they work all the time (even in 2015 edition).
|
||||
|
||||
/// ```rust
|
||||
/// #![feature(test)]
|
||||
///
|
||||
/// extern crate test;
|
||||
/// use test::Bencher;
|
||||
///
|
||||
/// #[bench]
|
||||
/// fn bench_xor_1000_ints(b: &mut Bencher) {
|
||||
/// b.iter(|| {
|
||||
/// (0..1000).fold(0, |old, new| old ^ new);
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
pub fn foo() {}
|
6
tests/rustdoc-ui/doctest/extern-crate.stdout
Normal file
6
tests/rustdoc-ui/doctest/extern-crate.stdout
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
running 1 test
|
||||
test $DIR/extern-crate.rs - foo (line 9) ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
|
||||
|
|
@ -1 +1 @@
|
|||
{"format_version":1,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":"#![allow(unused)]\nfn main() {\nlet x = 12;\nlet y = 14;\n}","name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":"#![allow(unused)]\nfn main() {\nlet\n}","name":"$DIR/extract-doctests.rs - (line 13)"}]}
|
||||
{"format_version":1,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":"#![allow(unused)]\nfn main() {\nlet x = 12;\nlet y = 14;\n}","name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":null,"name":"$DIR/extract-doctests.rs - (line 13)"}]}
|
|
@ -5,11 +5,11 @@ test remapped_path/remap-path-prefix-invalid-doctest.rs - SomeStruct (line 10) .
|
|||
failures:
|
||||
|
||||
---- remapped_path/remap-path-prefix-invalid-doctest.rs - SomeStruct (line 10) stdout ----
|
||||
error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `is`
|
||||
error: expected one of `!` or `::`, found `is`
|
||||
--> remapped_path/remap-path-prefix-invalid-doctest.rs:11:6
|
||||
|
|
||||
LL | this is not real code
|
||||
| ^^ expected one of 8 possible tokens
|
||||
| ^^ expected one of `!` or `::`
|
||||
|
||||
error: aborting due to 1 previous error
|
||||
|
||||
|
|
|
@ -24,4 +24,4 @@
|
|||
|
||||
//@ matches foo/index.html '//a[@class="test-arrow"][@href="https://www.example.com/?code=%23!%5Ballow(unused)%5D%0Afn+main()+%7B%0A++++println!(%22Hello,+world!%22);%0A%7D&edition=2015"]' ""
|
||||
//@ matches foo/index.html '//a[@class="test-arrow"][@href="https://www.example.com/?code=%23!%5Ballow(unused)%5D%0Afn+main()+%7B%0A++++println!(%22Hello,+world!%22);%0A%7D&edition=2015"]' ""
|
||||
//@ matches foo/index.html '//a[@class="test-arrow"][@href="https://www.example.com/?code=%23!%5Ballow(unused)%5D%0A%23!%5Bfeature(something)%5D%0A%0Afn+main()+%7B%0A++++println!(%22Hello,+world!%22);%0A%7D&version=nightly&edition=2015"]' ""
|
||||
//@ matches foo/index.html '//a[@class="test-arrow"][@href="https://www.example.com/?code=%23!%5Ballow(unused)%5D%0A%23!%5Bfeature(something)%5D%0A%0A%0Afn+main()+%7B%0A++++println!(%22Hello,+world!%22);%0A%7D&version=nightly&edition=2015"]' ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue