pedant-core 0.22.0

Analysis engine for pedant: IR extraction, style checks, and capability detection
Documentation
use std::sync::Arc;

use pedant_types::{
    Capability, CapabilityAnalysis, CapabilityFinding, ExecutionContext, FindingOrigin,
    SourceLocation,
};

use super::attribution::{CapabilityDraft, CapabilitySourceFact, project_analysis};
use super::paths::resolve_capabilities;
use super::strings::{KEY_MATERIAL_CHECKS, STRING_LITERAL_CHECKS, truncate_evidence};
use crate::ir::FileIr;
use crate::observe::{self, Observation};

struct FindingEmitter<'a> {
    draft: &'a mut CapabilityDraft,
    file: &'a Arc<str>,
    origin: FindingOrigin,
    execution_context: Option<ExecutionContext>,
}

impl FindingEmitter<'_> {
    fn emit(
        &mut self,
        capability: Capability,
        line: usize,
        column: usize,
        evidence: &str,
        containing_fn: Option<usize>,
    ) {
        let finding = CapabilityFinding {
            capability,
            location: SourceLocation {
                file: Arc::clone(self.file),
                line,
                column: column + 1,
            },
            evidence: Arc::from(evidence),
            origin: Some(self.origin),
            language: None,
            execution_context: self.execution_context,
            reachable: None,
        };
        self.draft.push(finding, containing_fn);
    }

    fn emit_from_facts<'a, T: CapabilitySourceFact + 'a>(
        &mut self,
        facts: &'a [T],
        mut mapper: impl FnMut(&'a T) -> Option<(Capability, usize, usize, &'a str)>,
    ) {
        for fact in facts {
            if let Some((capability, line, column, evidence)) = mapper(fact) {
                self.emit(capability, line, column, evidence, fact.containing_fn());
            }
        }
    }
}

/// Scan all IR facts for capability usage and produce a complete analysis.
///
/// When `execution_context` is present, every finding is tagged with that
/// context, such as [`ExecutionContext::BuildHook`] for `build.rs`.
///
/// The reported symbol attribution is
/// [`Complete`](pedant_types::SymbolAttributionStatus::Complete) relative to the
/// callable inventory of the supplied `ir`. `FileIr` is a public fact bag, so
/// this entry point proves classification against what the caller handed it,
/// not the provenance or freshness of that parse.
pub fn detect_capabilities(
    ir: &FileIr,
    execution_context: Option<ExecutionContext>,
) -> CapabilityAnalysis {
    project_analysis(ir, draft_capabilities(ir, execution_context))
}

/// Detect every capability finding, each paired with its callable owner.
///
/// Between this and [`project_analysis`] a caller may annotate the drafted
/// findings — semantic reachability is the one such pass — so that the flat and
/// symbol projections are sealed from the same settled values.
pub(crate) fn draft_capabilities(
    ir: &FileIr,
    execution_context: Option<ExecutionContext>,
) -> CapabilityDraft {
    let file_path = &ir.file_path;
    observe::record(Observation::CapabilityProjection(file_path));
    let mut draft = CapabilityDraft::default();

    detect_use_paths(ir, file_path, execution_context, &mut draft);
    detect_unsafe_sites(ir, file_path, execution_context, &mut draft);
    detect_extern_blocks(ir, file_path, execution_context, &mut draft);
    detect_attributes(ir, file_path, execution_context, &mut draft);
    detect_string_literals(ir, file_path, execution_context, &mut draft);

    draft
}

fn detect_use_paths(
    ir: &FileIr,
    file_path: &Arc<str>,
    execution_context: Option<ExecutionContext>,
    draft: &mut CapabilityDraft,
) {
    let mut emitter = FindingEmitter {
        draft,
        file: file_path,
        origin: FindingOrigin::Import,
        execution_context,
    };
    emitter.emit_from_facts(&ir.use_paths, |use_path| {
        resolve_capabilities(&use_path.path).map(|capability| {
            (
                capability,
                use_path.span.line,
                use_path.span.column,
                use_path.path.as_ref(),
            )
        })
    });
}

fn detect_unsafe_sites(
    ir: &FileIr,
    file_path: &Arc<str>,
    execution_context: Option<ExecutionContext>,
    draft: &mut CapabilityDraft,
) {
    let mut emitter = FindingEmitter {
        draft,
        file: file_path,
        origin: FindingOrigin::CodeSite,
        execution_context,
    };
    emitter.emit_from_facts(&ir.unsafe_sites, |site| {
        Some((
            Capability::UnsafeCode,
            site.span.line,
            site.span.column,
            site.evidence.as_ref(),
        ))
    });
}

fn detect_extern_blocks(
    ir: &FileIr,
    file_path: &Arc<str>,
    execution_context: Option<ExecutionContext>,
    draft: &mut CapabilityDraft,
) {
    let mut emitter = FindingEmitter {
        draft,
        file: file_path,
        origin: FindingOrigin::CodeSite,
        execution_context,
    };
    emitter.emit_from_facts(&ir.extern_blocks, |block| {
        Some((
            Capability::Ffi,
            block.span.line,
            block.span.column,
            "extern block",
        ))
    });
}

fn detect_attributes(
    ir: &FileIr,
    file_path: &Arc<str>,
    execution_context: Option<ExecutionContext>,
    draft: &mut CapabilityDraft,
) {
    let mut emitter = FindingEmitter {
        draft,
        file: file_path,
        origin: FindingOrigin::Attribute,
        execution_context,
    };
    emitter.emit_from_facts(&ir.attributes, |attribute| {
        let (capability, evidence) = match &*attribute.name {
            "link" => (Capability::Ffi, "#[link]"),
            "proc_macro" => (Capability::ProcMacro, "#[proc_macro]"),
            "proc_macro_derive" => (Capability::ProcMacro, "#[proc_macro_derive]"),
            "proc_macro_attribute" => (Capability::ProcMacro, "#[proc_macro_attribute]"),
            _ => return None,
        };
        Some((
            capability,
            attribute.span.line,
            attribute.span.column,
            evidence,
        ))
    });
}

fn detect_string_literals(
    ir: &FileIr,
    file_path: &Arc<str>,
    execution_context: Option<ExecutionContext>,
    draft: &mut CapabilityDraft,
) {
    let mut emitter = FindingEmitter {
        draft,
        file: file_path,
        origin: FindingOrigin::StringLiteral,
        execution_context,
    };
    for literal in &ir.string_literals {
        let line = literal.span.line;
        let column = literal.span.column;
        let owner = literal.containing_fn();

        if let Some(&(_, capability)) = STRING_LITERAL_CHECKS
            .iter()
            .find(|&&(checker, _)| checker(&literal.value))
        {
            emitter.emit(capability, line, column, &literal.value, owner);
        }
        if KEY_MATERIAL_CHECKS
            .iter()
            .any(|check| check(&literal.value))
        {
            let evidence = truncate_evidence(&literal.value);
            emitter.emit(Capability::Crypto, line, column, &evidence, owner);
        }
    }
}