//! A pass that annotates every item and method with its stability level,
//! propagating default levels lexically from parent to children ast nodes.

pub use self::StabilityLevel::*;

use crate::session::{DiagnosticMessageId, Session};
use crate::ty::{self, TyCtxt};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_errors::{Applicability, DiagnosticBuilder};
use rustc_feature::GateIssue;
use rustc_hir as hir;
use rustc_hir::def::DefKind;
use rustc_hir::def_id::{CrateNum, DefId, CRATE_DEF_INDEX};
use rustc_hir::{self, HirId};
use rustc_session::lint::{self, BuiltinLintDiagnostics, Lint, LintBuffer};
use rustc_span::symbol::{sym, Symbol};
use rustc_span::{MultiSpan, Span};
use syntax::ast::CRATE_NODE_ID;
use syntax::attr::{self, ConstStability, Deprecation, RustcDeprecation, Stability};
use syntax::sess::feature_err_issue;

use std::num::NonZeroU32;

#[derive(PartialEq, Clone, Copy, Debug)]
pub enum StabilityLevel {
    Unstable,
    Stable,
}

impl StabilityLevel {
    pub fn from_attr_level(level: &attr::StabilityLevel) -> Self {
        if level.is_stable() { Stable } else { Unstable }
    }
}

/// An entry in the `depr_map`.
#[derive(Clone, HashStable)]
pub struct DeprecationEntry {
    /// The metadata of the attribute associated with this entry.
    pub attr: Deprecation,
    /// The `DefId` where the attr was originally attached. `None` for non-local
    /// `DefId`'s.
    origin: Option<HirId>,
}

impl DeprecationEntry {
    pub fn local(attr: Deprecation, id: HirId) -> DeprecationEntry {
        DeprecationEntry { attr, origin: Some(id) }
    }

    pub fn external(attr: Deprecation) -> DeprecationEntry {
        DeprecationEntry { attr, origin: None }
    }

    pub fn same_origin(&self, other: &DeprecationEntry) -> bool {
        match (self.origin, other.origin) {
            (Some(o1), Some(o2)) => o1 == o2,
            _ => false,
        }
    }
}

/// A stability index, giving the stability level for items and methods.
#[derive(HashStable)]
pub struct Index<'tcx> {
    /// This is mostly a cache, except the stabilities of local items
    /// are filled by the annotator.
    pub stab_map: FxHashMap<HirId, &'tcx Stability>,
    pub const_stab_map: FxHashMap<HirId, &'tcx ConstStability>,
    pub depr_map: FxHashMap<HirId, DeprecationEntry>,

    /// Maps for each crate whether it is part of the staged API.
    pub staged_api: FxHashMap<CrateNum, bool>,

    /// Features enabled for this crate.
    pub active_features: FxHashSet<Symbol>,
}

impl<'tcx> Index<'tcx> {
    pub fn local_stability(&self, id: HirId) -> Option<&'tcx Stability> {
        self.stab_map.get(&id).cloned()
    }

    pub fn local_const_stability(&self, id: HirId) -> Option<&'tcx ConstStability> {
        self.const_stab_map.get(&id).cloned()
    }

    pub fn local_deprecation_entry(&self, id: HirId) -> Option<DeprecationEntry> {
        self.depr_map.get(&id).cloned()
    }
}

pub fn report_unstable(
    sess: &Session,
    feature: Symbol,
    reason: Option<Symbol>,
    issue: Option<NonZeroU32>,
    is_soft: bool,
    span: Span,
    soft_handler: impl FnOnce(&'static lint::Lint, Span, &str),
) {
    let msg = match reason {
        Some(r) => format!("use of unstable library feature '{}': {}", feature, r),
        None => format!("use of unstable library feature '{}'", &feature),
    };

    let msp: MultiSpan = span.into();
    let cm = &sess.parse_sess.source_map();
    let span_key = msp.primary_span().and_then(|sp: Span| {
        if !sp.is_dummy() {
            let file = cm.lookup_char_pos(sp.lo()).file;
            if file.name.is_macros() { None } else { Some(span) }
        } else {
            None
        }
    });

    let error_id = (DiagnosticMessageId::StabilityId(issue), span_key, msg.clone());
    let fresh = sess.one_time_diagnostics.borrow_mut().insert(error_id);
    if fresh {
        if is_soft {
            soft_handler(lint::builtin::SOFT_UNSTABLE, span, &msg)
        } else {
            feature_err_issue(&sess.parse_sess, feature, span, GateIssue::Library(issue), &msg)
                .emit();
        }
    }
}

/// Checks whether an item marked with `deprecated(since="X")` is currently
/// deprecated (i.e., whether X is not greater than the current rustc version).
pub fn deprecation_in_effect(since: &str) -> bool {
    fn parse_version(ver: &str) -> Vec<u32> {
        // We ignore non-integer components of the version (e.g., "nightly").
        ver.split(|c| c == '.' || c == '-').flat_map(|s| s.parse()).collect()
    }

    if let Some(rustc) = option_env!("CFG_RELEASE") {
        let since: Vec<u32> = parse_version(since);
        let rustc: Vec<u32> = parse_version(rustc);
        // We simply treat invalid `since` attributes as relating to a previous
        // Rust version, thus always displaying the warning.
        if since.len() != 3 {
            return true;
        }
        since <= rustc
    } else {
        // By default, a deprecation warning applies to
        // the current version of the compiler.
        true
    }
}

pub fn deprecation_suggestion(
    diag: &mut DiagnosticBuilder<'_>,
    suggestion: Option<Symbol>,
    span: Span,
) {
    if let Some(suggestion) = suggestion {
        diag.span_suggestion(
            span,
            "replace the use of the deprecated item",
            suggestion.to_string(),
            Applicability::MachineApplicable,
        );
    }
}

fn deprecation_message_common(message: String, reason: Option<Symbol>) -> String {
    match reason {
        Some(reason) => format!("{}: {}", message, reason),
        None => message,
    }
}

pub fn deprecation_message(depr: &Deprecation, path: &str) -> (String, &'static Lint) {
    let message = format!("use of deprecated item '{}'", path);
    (deprecation_message_common(message, depr.note), lint::builtin::DEPRECATED)
}

pub fn rustc_deprecation_message(depr: &RustcDeprecation, path: &str) -> (String, &'static Lint) {
    let (message, lint) = if deprecation_in_effect(&depr.since.as_str()) {
        (format!("use of deprecated item '{}'", path), lint::builtin::DEPRECATED)
    } else {
        (
            format!(
                "use of item '{}' that will be deprecated in future version {}",
                path, depr.since
            ),
            lint::builtin::DEPRECATED_IN_FUTURE,
        )
    };
    (deprecation_message_common(message, Some(depr.reason)), lint)
}

pub fn early_report_deprecation(
    lint_buffer: &'a mut LintBuffer,
    message: &str,
    suggestion: Option<Symbol>,
    lint: &'static Lint,
    span: Span,
) {
    if span.in_derive_expansion() {
        return;
    }

    let diag = BuiltinLintDiagnostics::DeprecatedMacro(suggestion, span);
    lint_buffer.buffer_lint_with_diagnostic(lint, CRATE_NODE_ID, span, message, diag);
}

fn late_report_deprecation(
    tcx: TyCtxt<'_>,
    message: &str,
    suggestion: Option<Symbol>,
    lint: &'static Lint,
    span: Span,
    def_id: DefId,
    hir_id: HirId,
) {
    if span.in_derive_expansion() {
        return;
    }

    let mut diag = tcx.struct_span_lint_hir(lint, hir_id, span, message);
    if let hir::Node::Expr(_) = tcx.hir().get(hir_id) {
        deprecation_suggestion(&mut diag, suggestion, span);
    }
    diag.emit();
    if hir_id == hir::DUMMY_HIR_ID {
        span_bug!(span, "emitted a {} lint with dummy HIR id: {:?}", lint.name, def_id);
    }
}

/// Result of `TyCtxt::eval_stability`.
pub enum EvalResult {
    /// We can use the item because it is stable or we provided the
    /// corresponding feature gate.
    Allow,
    /// We cannot use the item because it is unstable and we did not provide the
    /// corresponding feature gate.
    Deny { feature: Symbol, reason: Option<Symbol>, issue: Option<NonZeroU32>, is_soft: bool },
    /// The item does not have the `#[stable]` or `#[unstable]` marker assigned.
    Unmarked,
}

// See issue #38412.
fn skip_stability_check_due_to_privacy(tcx: TyCtxt<'_>, mut def_id: DefId) -> bool {
    // Check if `def_id` is a trait method.
    match tcx.def_kind(def_id) {
        Some(DefKind::Method) | Some(DefKind::AssocTy) | Some(DefKind::AssocConst) => {
            if let ty::TraitContainer(trait_def_id) = tcx.associated_item(def_id).container {
                // Trait methods do not declare visibility (even
                // for visibility info in cstore). Use containing
                // trait instead, so methods of `pub` traits are
                // themselves considered `pub`.
                def_id = trait_def_id;
            }
        }
        _ => {}
    }

    let visibility = tcx.visibility(def_id);

    match visibility {
        // Must check stability for `pub` items.
        ty::Visibility::Public => false,

        // These are not visible outside crate; therefore
        // stability markers are irrelevant, if even present.
        ty::Visibility::Restricted(..) | ty::Visibility::Invisible => true,
    }
}

impl<'tcx> TyCtxt<'tcx> {
    /// Evaluates the stability of an item.
    ///
    /// Returns `EvalResult::Allow` if the item is stable, or unstable but the corresponding
    /// `#![feature]` has been provided. Returns `EvalResult::Deny` which describes the offending
    /// unstable feature otherwise.
    ///
    /// If `id` is `Some(_)`, this function will also check if the item at `def_id` has been
    /// deprecated. If the item is indeed deprecated, we will emit a deprecation lint attached to
    /// `id`.
    pub fn eval_stability(self, def_id: DefId, id: Option<HirId>, span: Span) -> EvalResult {
        // Deprecated attributes apply in-crate and cross-crate.
        if let Some(id) = id {
            if let Some(depr_entry) = self.lookup_deprecation_entry(def_id) {
                let parent_def_id = self.hir().local_def_id(self.hir().get_parent_item(id));
                let skip = self
                    .lookup_deprecation_entry(parent_def_id)
                    .map_or(false, |parent_depr| parent_depr.same_origin(&depr_entry));

                if !skip {
                    let (message, lint) =
                        deprecation_message(&depr_entry.attr, &self.def_path_str(def_id));
                    late_report_deprecation(self, &message, None, lint, span, def_id, id);
                }
            };
        }

        let is_staged_api =
            self.lookup_stability(DefId { index: CRATE_DEF_INDEX, ..def_id }).is_some();
        if !is_staged_api {
            return EvalResult::Allow;
        }

        let stability = self.lookup_stability(def_id);
        debug!(
            "stability: \
                inspecting def_id={:?} span={:?} of stability={:?}",
            def_id, span, stability
        );

        if let Some(id) = id {
            if let Some(stability) = stability {
                if let Some(depr) = &stability.rustc_depr {
                    let (message, lint) =
                        rustc_deprecation_message(depr, &self.def_path_str(def_id));
                    late_report_deprecation(
                        self,
                        &message,
                        depr.suggestion,
                        lint,
                        span,
                        def_id,
                        id,
                    );
                }
            }
        }

        // Only the cross-crate scenario matters when checking unstable APIs
        let cross_crate = !def_id.is_local();
        if !cross_crate {
            return EvalResult::Allow;
        }

        // Issue #38412: private items lack stability markers.
        if skip_stability_check_due_to_privacy(self, def_id) {
            return EvalResult::Allow;
        }

        match stability {
            Some(&Stability {
                level: attr::Unstable { reason, issue, is_soft }, feature, ..
            }) => {
                if span.allows_unstable(feature) {
                    debug!("stability: skipping span={:?} since it is internal", span);
                    return EvalResult::Allow;
                }
                if self.stability().active_features.contains(&feature) {
                    return EvalResult::Allow;
                }

                // When we're compiling the compiler itself we may pull in
                // crates from crates.io, but those crates may depend on other
                // crates also pulled in from crates.io. We want to ideally be
                // able to compile everything without requiring upstream
                // modifications, so in the case that this looks like a
                // `rustc_private` crate (e.g., a compiler crate) and we also have
                // the `-Z force-unstable-if-unmarked` flag present (we're
                // compiling a compiler crate), then let this missing feature
                // annotation slide.
                if feature == sym::rustc_private && issue == NonZeroU32::new(27812) {
                    if self.sess.opts.debugging_opts.force_unstable_if_unmarked {
                        return EvalResult::Allow;
                    }
                }

                EvalResult::Deny { feature, reason, issue, is_soft }
            }
            Some(_) => {
                // Stable APIs are always ok to call and deprecated APIs are
                // handled by the lint emitting logic above.
                EvalResult::Allow
            }
            None => EvalResult::Unmarked,
        }
    }

    /// Checks if an item is stable or error out.
    ///
    /// If the item defined by `def_id` is unstable and the corresponding `#![feature]` does not
    /// exist, emits an error.
    ///
    /// Additionally, this function will also check if the item is deprecated. If so, and `id` is
    /// not `None`, a deprecated lint attached to `id` will be emitted.
    pub fn check_stability(self, def_id: DefId, id: Option<HirId>, span: Span) {
        let soft_handler =
            |lint, span, msg: &_| self.lint_hir(lint, id.unwrap_or(hir::CRATE_HIR_ID), span, msg);
        match self.eval_stability(def_id, id, span) {
            EvalResult::Allow => {}
            EvalResult::Deny { feature, reason, issue, is_soft } => {
                report_unstable(self.sess, feature, reason, issue, is_soft, span, soft_handler)
            }
            EvalResult::Unmarked => {
                // The API could be uncallable for other reasons, for example when a private module
                // was referenced.
                self.sess.delay_span_bug(span, &format!("encountered unmarked API: {:?}", def_id));
            }
        }
    }

    pub fn lookup_deprecation(self, id: DefId) -> Option<Deprecation> {
        self.lookup_deprecation_entry(id).map(|depr| depr.attr)
    }
}
