#![allow(non_local_definitions)]
use anyhow::Error;
use clap::ValueEnum;
use rustc_data_structures::fx::FxHashSet;
use rustc_hash::FxHashMap;
use rustc_hir::def_id::{CrateNum, LOCAL_CRATE};
use rustc_middle::ty::TyCtxt;
use rustc_span::Symbol;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::OnceLock;
use cargo_paralegal_flow::{
ClapAnalysisCtrl, ClapArgs, Debugger, DumpOption, MarkerControl, ParseableDumpArgs,
};
use flowistry_pdg_construction::source_access::std_crates;
use paralegal_spdg::utils::setup_logging;
use crate::utils::TinyBitSet;
#[derive(thiserror::Error, Debug)]
enum VarError {
#[error("env variable value is not unicode, approximate key and value are {}: {}", key.to_string_lossy(), value.to_string_lossy())]
NotUnicode { key: OsString, value: OsString },
}
fn env_var_expect_unicode(k: impl AsRef<OsStr>) -> Result<Option<String>, VarError> {
let k_ref = k.as_ref();
match std::env::var(k_ref) {
Ok(v) => Ok(Some(v)),
Err(std::env::VarError::NotUnicode(u)) => Err(VarError::NotUnicode {
key: k_ref.to_owned(),
value: u,
}),
Err(std::env::VarError::NotPresent) => Ok(None),
}
}
impl TryFrom<ClapArgs> for Args {
type Error = Error;
fn try_from(value: ClapArgs) -> Result<Self, Self::Error> {
let build_config: (_, BuildConfig) = if let Some(conf) = value.get_build_config() {
let absolute = conf.canonicalize()?;
let config = toml::from_str(&std::fs::read_to_string(&absolute)?)?;
(Some(absolute), config)
} else {
Default::default()
};
let ClapArgs {
result_path,
relaxed,
target,
abort_after_analysis,
mut anactrl,
dump,
marker_control,
cargo_args,
attach_to_debugger,
strict,
build_config: _,
} = value;
if relaxed {
eprintln!("The `--relaxed` flag is deprecated. This is now the default behavior and therefore the flag is ignored.");
}
let mut dump: DumpArgs = dump.into();
if let Some(from_env) = env_var_expect_unicode("PARALEGAL_DUMP")? {
let from_env = DumpArgs::from_str(&from_env).map_err(|s| anyhow::anyhow!("{}", s))?;
dump.0 |= from_env.0;
}
anactrl.analyze = anactrl
.analyze
.iter()
.flat_map(|s| s.split(',').map(ToOwned::to_owned))
.collect();
if let Some(from_env) = env_var_expect_unicode("PARALEGAL_ANALYZE")? {
anactrl
.analyze
.extend(from_env.split(',').map(ToOwned::to_owned));
}
anactrl
.include
.extend(build_config.1.include.iter().cloned());
Ok(Args {
result_path,
relaxed: !strict,
target,
abort_after_analysis,
anactrl: anactrl.try_into()?,
dump,
build_config,
marker_control,
cargo_args,
attach_to_debugger,
})
}
}
pub struct Args {
result_path: std::path::PathBuf,
relaxed: bool,
target: Option<String>,
abort_after_analysis: bool,
attach_to_debugger: Option<Debugger>,
marker_control: MarkerControl,
anactrl: AnalysisCtrl,
dump: DumpArgs,
build_config: (Option<PathBuf>, BuildConfig),
cargo_args: Vec<String>,
}
impl Default for Args {
fn default() -> Self {
Self {
result_path: PathBuf::from(paralegal_spdg::FLOW_GRAPH_OUT_NAME),
relaxed: true,
target: None,
abort_after_analysis: false,
marker_control: Default::default(),
anactrl: Default::default(),
dump: Default::default(),
build_config: Default::default(),
cargo_args: Vec::new(),
attach_to_debugger: None,
}
}
}
impl From<DumpOption> for DumpArgs {
fn from(value: DumpOption) -> Self {
[value].into_iter().collect()
}
}
impl From<ParseableDumpArgs> for DumpArgs {
fn from(value: ParseableDumpArgs) -> Self {
value.dump.into_iter().collect()
}
}
#[derive(Clone, Default)]
pub struct DumpArgs(TinyBitSet);
impl FromStr for DumpArgs {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.split(",")
.map(|opt| DumpOption::from_str(opt, true))
.collect()
}
}
impl FromIterator<DumpOption> for DumpArgs {
fn from_iter<T: IntoIterator<Item = DumpOption>>(iter: T) -> Self {
Self(iter.into_iter().map(|v| v as u32).collect())
}
}
impl Args {
pub fn target(&self) -> Option<&str> {
self.target.as_deref()
}
pub fn dbg(&self) -> &DumpArgs {
&self.dump
}
pub fn anactrl(&self) -> &AnalysisCtrl {
&self.anactrl
}
pub fn result_path(&self) -> &std::path::Path {
self.result_path.as_path()
}
pub fn relaxed(&self) -> bool {
self.relaxed
}
pub fn abort_after_analysis(&self) -> bool {
self.abort_after_analysis
}
pub fn build_config(&self) -> &BuildConfig {
&self.build_config.1
}
pub fn marker_control(&self) -> &MarkerControl {
&self.marker_control
}
pub fn cargo_args(&self) -> &[String] {
&self.cargo_args
}
pub fn attach_to_debugger(&self) -> Option<Debugger> {
self.attach_to_debugger
}
pub fn setup_logging(&self) -> anyhow::Result<()> {
setup_logging()
}
}
pub struct AnalysisCtrl {
analyze: Vec<String>,
inlining_depth: InliningDepth,
include: Vec<String>,
included_crate_cache: OnceLock<FxHashSet<CrateNum>>,
no_pdg_cache: bool,
include_std: bool,
}
impl Default for AnalysisCtrl {
fn default() -> Self {
Self {
analyze: Vec::new(),
inlining_depth: InliningDepth::Adaptive(0),
include: Default::default(),
no_pdg_cache: false,
included_crate_cache: OnceLock::new(),
include_std: false,
}
}
}
impl TryFrom<ClapAnalysisCtrl> for AnalysisCtrl {
type Error = Error;
fn try_from(value: ClapAnalysisCtrl) -> Result<Self, Self::Error> {
let ClapAnalysisCtrl {
analyze,
include,
no_pdg_cache,
no_interprocedural_analysis,
no_adaptive_approximation,
k_depth,
include_std,
} = value;
let inlining_depth = if no_interprocedural_analysis {
InliningDepth::K(0)
} else if no_adaptive_approximation {
k_depth.map_or(InliningDepth::Unconstrained, InliningDepth::K)
} else {
InliningDepth::Adaptive(k_depth.unwrap_or(0))
};
Ok(Self {
analyze,
inlining_depth,
include,
no_pdg_cache,
included_crate_cache: OnceLock::new(),
include_std,
})
}
}
#[derive(strum::EnumIs, strum::AsRefStr, Clone)]
pub enum InliningDepth {
Unconstrained,
K(u32),
Adaptive(u32),
}
impl AnalysisCtrl {
pub fn selected_targets(&self) -> &[String] {
&self.analyze
}
pub fn use_recursive_analysis(&self) -> bool {
!matches!(self.inlining_depth, InliningDepth::K(0))
}
pub fn inlining_depth(&self) -> &InliningDepth {
&self.inlining_depth
}
pub fn included(&self, crate_name: &str) -> bool {
if self.include.is_empty() {
true
} else {
self.include.iter().any(|s| s == crate_name)
}
}
fn crate_inclusion_set<'a>(&'a self, tcx: TyCtxt<'_>) -> &'a FxHashSet<CrateNum> {
self.included_crate_cache
.get_or_init(|| {
if self.include.is_empty() {
let std_crates = if self.include_std {
Default::default()
} else {
std_crates(tcx).collect::<FxHashSet<_>>()
};
tcx.crates(())
.iter()
.copied()
.filter(move |c| !std_crates.contains(c))
.chain([LOCAL_CRATE])
.collect()
} else {
let mut included_crate_names = self
.include
.iter()
.filter(|c| c.as_str() != "crate")
.map(|s| (Symbol::intern(s), false))
.collect::<FxHashMap<_, bool>>();
let set = tcx.crates(())
.iter()
.copied()
.filter(|cnum| included_crate_names.get_mut(&tcx.crate_name(*cnum)).is_some_and(|v| {
*v = true;
true
}))
.chain([LOCAL_CRATE])
.collect();
for (k, v) in included_crate_names {
if !v {
tcx.dcx().warn(format!("The crate `{k}` was configured for inclusion but is not part of the dependencies."));
}
}
set
}
})
}
pub fn included_crates<'a>(&'a self, tcx: TyCtxt<'_>) -> impl Iterator<Item = CrateNum> + 'a {
self.crate_inclusion_set(tcx).iter().copied()
}
pub fn inclusion_predicate(&self, tcx: TyCtxt<'_>) -> impl Fn(CrateNum) -> bool {
let included_crates = self.crate_inclusion_set(tcx).clone();
move |cnum| included_crates.contains(&cnum)
}
pub fn pdg_cache(&self) -> bool {
!self.no_pdg_cache
}
pub fn include_std(&self) -> bool {
self.include_std
}
}
impl DumpArgs {
#[inline]
fn has(&self, opt: DumpOption) -> bool {
self.0.contains(DumpOption::All as u32).unwrap() || self.0.contains(opt as u32).unwrap()
}
pub fn dump_flowistry_pdg(&self) -> bool {
self.has(DumpOption::FlowistryPdg)
}
pub fn dump_spdg(&self) -> bool {
self.has(DumpOption::Spdg)
}
pub fn dump_mir(&self) -> bool {
self.has(DumpOption::Mir)
}
}
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct DepConfig {
#[serde(default, alias = "rust-features")]
pub rust_features: Box<[String]>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(tag = "mode", rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub enum Stub {
#[serde(rename_all = "kebab-case")]
SubClosure { generic_name: String },
#[serde(rename_all = "kebab-case")]
SubFuture { generic_name: String },
}
#[derive(serde::Deserialize, serde::Serialize, Default, Debug)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct BuildConfig {
#[serde(default)]
pub dep: crate::HashMap<String, DepConfig>,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub stubs: HashMap<String, Stub>,
}